Skip to content

Commit 6b18db4

Browse files
committed
fix(kysely-generic-sqlite): bind queryId into worker message
1 parent 377c66d commit 6b18db4

4 files changed

Lines changed: 127 additions & 38 deletions

File tree

packages/dialect-generic-sqlite/src/worker/driver.ts

Lines changed: 81 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class GenericSqliteWorkerDriver<
4040
this.worker.postMessage([initEvent, exec.data || ({} as any)] satisfies InitMsg<R>)
4141

4242
await new Promise<void>((resolve, reject) => {
43-
this.mitt!.once(initEvent, (_, err) => (err ? reject(err) : resolve()))
43+
this.mitt!.once(initEvent, (_qid, _data, err) => (err ? reject(err) : resolve()))
4444
})
4545

4646
this.conn = new GenericSqliteWorkerConnection(this.worker, this.mitt)
@@ -54,7 +54,7 @@ export class GenericSqliteWorkerDriver<
5454
}
5555
this.worker.postMessage([closeEvent] satisfies CloseMsg)
5656
return new Promise<void>((resolve, reject) =>
57-
this.mitt?.once(closeEvent, (_, err) => (err ? reject(err) : resolve())),
57+
this.mitt?.once(closeEvent, (_qid, _data, err) => (err ? reject(err) : resolve())),
5858
).finally(() => {
5959
this.worker?.terminate()
6060
this.mitt?.off()
@@ -64,13 +64,71 @@ export class GenericSqliteWorkerDriver<
6464
}
6565

6666
class GenericSqliteWorkerConnection implements DatabaseConnection {
67+
private pendingRuns = new Map<
68+
string,
69+
{ resolve: (data: any) => void; reject: (err: any) => void }
70+
>()
71+
private pendingStreams = new Map<
72+
string,
73+
{ resolve: (data: any) => void; reject: (err: any) => void }
74+
>()
75+
6776
constructor(
6877
readonly worker: IGenericWorker,
6978
readonly mitt: IGenericEventEmitter,
70-
) {}
79+
) {
80+
// Central dispatcher — register once, route by queryId
81+
this.mitt.on(runEvent, (queryId: string | undefined, data: any, err: unknown) => {
82+
if (!queryId) {
83+
return
84+
}
85+
const pending = this.pendingRuns.get(queryId)
86+
if (!pending) {
87+
return
88+
}
89+
this.pendingRuns.delete(queryId)
90+
if (err) {
91+
pending.reject(err)
92+
} else {
93+
pending.resolve(data)
94+
}
95+
})
96+
97+
this.mitt.on(dataEvent, (queryId: string | undefined, data: any, err: unknown) => {
98+
if (!queryId) {
99+
return
100+
}
101+
const state = this.pendingStreams.get(queryId)
102+
if (!state) {
103+
return
104+
}
105+
if (err) {
106+
this.pendingStreams.delete(queryId)
107+
state.reject(err)
108+
} else {
109+
state.resolve([{ rows: [data] }, false])
110+
}
111+
})
112+
113+
this.mitt.on(endEvent, (queryId: string | undefined, _data: any, err: unknown) => {
114+
if (!queryId) {
115+
return
116+
}
117+
const state = this.pendingStreams.get(queryId)
118+
if (!state) {
119+
return
120+
}
121+
this.pendingStreams.delete(queryId)
122+
if (err) {
123+
state.reject(err)
124+
} else {
125+
state.resolve([undefined, true])
126+
}
127+
})
128+
}
71129

72130
async *streamQuery<R>(
73-
{ parameters, sql, query }: CompiledQuery,
131+
{ parameters, sql, query, queryId }: CompiledQuery,
74132
chunkSize?: number,
75133
options?: AbortableOperationOptions,
76134
): AsyncIterableIterator<QueryResult<R>> {
@@ -80,40 +138,31 @@ class GenericSqliteWorkerConnection implements DatabaseConnection {
80138

81139
this.worker.postMessage([
82140
dataEvent,
141+
queryId.queryId,
83142
SelectQueryNode.is(query),
84143
sql,
85144
parameters,
86145
chunkSize,
87146
] satisfies StreamMsg)
88147
type ResolveData = [data: QueryResult<R> | undefined, done: boolean]
89148
let done = false
90-
let resolveFn: (value: ResolveData) => void
91149
let rejectFn: (reason?: any) => void
92150

93151
const onAbort = (): void => rejectFn(new Error('Query aborted'))
94152
options?.signal?.addEventListener('abort', onAbort, { once: true })
95153

96-
this.mitt!.on(dataEvent, (data, err): void => {
97-
if (err) {
98-
rejectFn(err)
99-
} else {
100-
resolveFn([{ rows: [data] }, false])
101-
}
102-
})
103-
104-
this.mitt!.on(endEvent, (_, err): void => {
105-
if (err) {
106-
rejectFn(err)
107-
} else {
108-
resolveFn([undefined, true])
109-
}
110-
})
154+
const streamState: { resolve: (data: any) => void; reject: (err: any) => void } = {
155+
resolve: undefined!,
156+
reject: undefined!,
157+
}
158+
this.pendingStreams.set(queryId.queryId, streamState)
111159

112160
try {
113161
while (!done) {
114162
const [data, isDone] = await new Promise<ResolveData>((res, rej) => {
115-
resolveFn = res
116163
rejectFn = rej
164+
streamState.resolve = res
165+
streamState.reject = rej
117166
})
118167

119168
if (isDone) {
@@ -124,20 +173,22 @@ class GenericSqliteWorkerConnection implements DatabaseConnection {
124173
}
125174
} finally {
126175
options?.signal?.removeEventListener('abort', onAbort)
127-
this.mitt?.off(dataEvent)
128-
this.mitt?.off(endEvent)
176+
this.pendingStreams.delete(queryId.queryId)
129177
}
130178
}
131179

132180
async executeQuery<R>(compiledQuery: CompiledQuery<unknown>): Promise<QueryResult<R>> {
133-
const { parameters, sql, query } = compiledQuery
134-
this.worker.postMessage([runEvent, SelectQueryNode.is(query), sql, parameters] satisfies RunMsg)
135-
return await new Promise((resolve, reject) => {
136-
if (!this.mitt) {
137-
reject(new Error('kysely instance has been destroyed'))
138-
}
181+
const { parameters, sql, query, queryId } = compiledQuery
182+
this.worker.postMessage([
183+
runEvent,
184+
queryId.queryId,
185+
SelectQueryNode.is(query),
186+
sql,
187+
parameters,
188+
] satisfies RunMsg)
139189

140-
this.mitt!.once(runEvent, (data, err) => (!err && data ? resolve(data) : reject(err)))
190+
return new Promise((resolve, reject) => {
191+
this.pendingRuns.set(queryId.queryId, { resolve, reject })
141192
})
142193
}
143194
}

packages/dialect-generic-sqlite/src/worker/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ export type InitMsg<T extends Record<string, unknown>> = [type: typeof initEvent
1212

1313
export type RunMsg = [
1414
type: typeof runEvent,
15+
queryId: string,
1516
isSelect: boolean,
1617
sql: string,
1718
parameters?: readonly unknown[],
1819
]
1920

2021
export type StreamMsg = [
2122
type: typeof dataEvent,
23+
queryId: string,
2224
isSelect: boolean,
2325
sql: string,
2426
parameters?: readonly unknown[],
@@ -34,7 +36,7 @@ export type MainToWorkerMsg<T extends Record<string, unknown>> =
3436
| StreamMsg
3537

3638
export type WorkerToMainMsg = {
37-
[K in keyof Events]: [type: `${K}`, data: Events[K], err: unknown]
39+
[K in keyof Events]: [type: `${K}`, queryId: string | null, data: Events[K], err: unknown]
3840
}[keyof Events]
3941

4042
type Events = {

packages/dialect-generic-sqlite/src/worker/utils.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,17 @@ export function createGenericOnMessageCallback<T extends Record<string, unknown>
1515
message?: MessageHandleFn<DB>,
1616
): (data: MainToWorkerMsg<T>) => Promise<void> {
1717
let db: IGenericSqlite<DB>
18-
return async ([type, data1, data2, data3, data4]) => {
19-
const ret: WorkerToMainMsg = [type, null, null]
18+
return async ([type, data1, data2, data3, data4, data5]) => {
19+
const ret: WorkerToMainMsg = [type, null, null, null]
2020
try {
2121
switch (type) {
2222
case initEvent:
2323
db = await init(data1)
2424
break
2525

2626
case runEvent:
27-
ret[1] = await db.query(data1, data2, data3)
27+
ret[1] = data1 // queryId
28+
ret[2] = await db.query(data2, data3, data4)
2829
break
2930

3031
case closeEvent:
@@ -35,23 +36,24 @@ export function createGenericOnMessageCallback<T extends Record<string, unknown>
3536
if (!db.iterator) {
3637
throw new Error('streamQuery() is not supported.')
3738
}
38-
const it = db.iterator(data1, data2, data3, data4)
39+
const it = db.iterator(data2, data3, data4, data5)
3940
for await (const row of it) {
40-
post([type, row as any, null] satisfies WorkerToMainMsg)
41+
post([type, data1, row as any, null] satisfies WorkerToMainMsg)
4142
}
4243
ret[0] = endEvent
44+
ret[1] = data1 // queryId
4345
break
4446
}
4547
default:
4648
if (message) {
4749
const data = await message(db, type, data1, data2, data3, data4)
4850
if (data !== undefined && data !== null) {
49-
ret[1] = data
51+
ret[2] = data
5052
}
5153
}
5254
}
5355
} catch (error) {
54-
ret[2] = error
56+
ret[3] = error
5557
}
5658
post(ret)
5759
}

packages/dialect-generic-sqlite/test/index.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { describe, expect, it } from 'vitest'
33

44
import { testCase } from '../../test-utils'
55
import { buildQueryFn, GenericSqliteDialect, parseBigInt } from '../src'
6+
import { createGenericOnMessageCallback } from '../src/worker'
7+
import { initEvent, dataEvent, runEvent } from '../src/worker/types'
68

79
describe('generic sqlite dialect test', () => {
810
it('better-sqlite3 executor', async () => {
@@ -31,4 +33,36 @@ describe('generic sqlite dialect test', () => {
3133

3234
await testCase(dialect, expect, false)
3335
})
36+
it('keeps worker query responses isolated by query id', async () => {
37+
const messages: unknown[] = []
38+
const onMessage = createGenericOnMessageCallback(
39+
async () => ({
40+
db: {},
41+
close: () => {},
42+
query: async (_isSelect, _sql, parameters) => ({
43+
rows: [{ id: parameters?.[0] }],
44+
}),
45+
*iterator(_isSelect, _sql, parameters, chunkSize) {
46+
yield {
47+
chunkSize,
48+
id: parameters?.[0],
49+
}
50+
},
51+
}),
52+
(message) => messages.push(message),
53+
)
54+
55+
await onMessage([initEvent, {}])
56+
await onMessage([runEvent, 'query-a', true, 'select ?', ['a']])
57+
await onMessage([runEvent, 'query-b', true, 'select ?', ['b']])
58+
await onMessage([dataEvent, 'stream-a', true, 'select ?', ['a'], 2])
59+
60+
expect(messages).toStrictEqual([
61+
[initEvent, null, null, null],
62+
[runEvent, 'query-a', { rows: [{ id: 'a' }] }, null],
63+
[runEvent, 'query-b', { rows: [{ id: 'b' }] }, null],
64+
[dataEvent, 'stream-a', { chunkSize: 2, id: 'a' }, null],
65+
['4', 'stream-a', null, null],
66+
])
67+
})
3468
})

0 commit comments

Comments
 (0)