-
Notifications
You must be signed in to change notification settings - Fork 70
Expand file tree
/
Copy pathforce_close_db_lock_abort.test.ts
More file actions
114 lines (102 loc) · 4.06 KB
/
force_close_db_lock_abort.test.ts
File metadata and controls
114 lines (102 loc) · 4.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import * as Comlink from 'comlink';
import { describe, expect, it, vi } from 'vitest';
import { LockedAsyncDatabaseAdapter } from '../src/db/adapters/LockedAsyncDatabaseAdapter';
import { WorkerWrappedAsyncDatabaseConnection } from '../src/db/adapters/WorkerWrappedAsyncDatabaseConnection';
import type { AsyncDatabaseConnection, OpenAsyncDatabaseConnection } from '../src/db/adapters/AsyncDatabaseConnection';
import {
DEFAULT_WEB_SQL_FLAGS,
TemporaryStorageOption,
type ResolvedWebSQLOpenOptions
} from '../src/db/adapters/web-sql-flags';
const baseConfig: ResolvedWebSQLOpenOptions = {
dbFilename: 'crm.sqlite',
flags: DEFAULT_WEB_SQL_FLAGS,
temporaryStorage: TemporaryStorageOption.MEMORY,
cacheSizeKb: 1
};
const baseConnection: AsyncDatabaseConnection = {
init: async () => {},
close: async () => {},
markHold: async () => 'hold',
releaseHold: async () => {},
isAutoCommit: async () => true,
execute: async () => ({ rows: { _array: [], length: 0 }, rowsAffected: 0, insertId: 0 }),
executeRaw: async () => [],
executeBatch: async () => ({ rows: { _array: [], length: 0 }, rowsAffected: 0, insertId: 0 }),
registerOnTableChange: async () => () => {},
getConfig: async () => baseConfig
};
describe('forceClose db-lock abort', () => {
it('aborts pending db-lock when forceClose happens after lock request', async () => {
let lockRequestedResolve!: () => void;
const lockRequested = new Promise<void>((resolve) => {
lockRequestedResolve = resolve;
});
const mockLocks = {
request: ((...args: any[]) => {
const [name, optionsOrCallback, callback] = args;
const lockCallback = typeof optionsOrCallback === 'function' ? optionsOrCallback : callback;
const signal =
typeof optionsOrCallback === 'object' && optionsOrCallback ? optionsOrCallback.signal : undefined;
return new Promise((resolve, reject) => {
const onAbort = () => {
reject(new DOMException('Aborted', 'AbortError'));
};
if (signal) {
signal.addEventListener('abort', onAbort);
}
Promise.resolve()
.then(async () => {
const callbackPromise = lockCallback?.({ name, mode: 'exclusive' });
lockRequestedResolve();
return callbackPromise;
})
.then(resolve)
.catch(reject)
.finally(() => {
if (signal) {
signal.removeEventListener('abort', onAbort);
}
});
});
}) as LockManager['request'],
query: vi.fn()
} as LockManager;
const locksSpy = vi.spyOn(navigator, 'locks', 'get').mockReturnValue(mockLocks);
const hangingExecute = vi.fn(() => new Promise<never>(() => {}));
const connection: AsyncDatabaseConnection = { ...baseConnection, execute: hangingExecute };
const remote = {
[Comlink.releaseProxy]: () => {}
} as Comlink.Remote<OpenAsyncDatabaseConnection<ResolvedWebSQLOpenOptions>>;
let wrapped: WorkerWrappedAsyncDatabaseConnection | null = null;
const adapter = new LockedAsyncDatabaseAdapter({
name: 'crm.sqlite',
openConnection: async () => {
wrapped = new WorkerWrappedAsyncDatabaseConnection({
baseConnection: connection,
identifier: 'crm.sqlite',
remoteCanCloseUnexpectedly: false,
remote
});
return wrapped;
}
});
const executePromise = adapter.execute('select 1');
const executeOutcome = executePromise.then(
() => ({ status: 'resolved' as const }),
(error) => ({ status: 'rejected' as const, error })
);
await lockRequested;
wrapped!.forceClose();
const outcome = await Promise.race([
executeOutcome,
new Promise<{ status: 'timeout' }>((resolve) => setTimeout(() => resolve({ status: 'timeout' }), 150))
]);
locksSpy.mockRestore();
expect(outcome.status).not.toBe('timeout');
expect(outcome.status).toBe('rejected');
if (outcome.status === 'rejected') {
expect(outcome.error?.name).toBe('AbortError');
}
});
});