@playcraft/cli 0.0.58 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,27 @@
1
- import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
1
+ import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
2
3
  import { dirname, join } from 'node:path';
3
- import { workspaceRegistrationSchema, checkpointRecordSchema, operationRecordSchema, pendingSourceEventSchema, sourceEventKey, sourceEventReceiptSchema, terminalRecordSchema, } from '@playcraft/common/cloud-workspace/runtime';
4
+ import { workspaceRegistrationSchema, checkpointRecordSchema, operationRecordSchema, pendingSourceEventSchema, replayPinResultSchema, retainedRangeHasGap, sourceAckSchema, sourceEventKey, sourceEventReceiptSchema, terminalRecordSchema, } from '@playcraft/common/cloud-workspace/runtime';
5
+ import { SqliteWorkerClient } from './sqlite-client.js';
6
+ export const RUNTIME_STATE_FILE = 'runtime-state.sqlite';
7
+ export const LEGACY_JOURNAL_FILE = 'journal.json';
8
+ export const LEGACY_JOURNAL_SENTINEL = 'PLAYCRAFT_RUNTIME_STATE_SQLITE\n';
4
9
  export class RuntimeJournal {
5
10
  filePath;
6
- writes = Promise.resolve();
11
+ legacyPath;
12
+ client;
13
+ listeners = new Set();
14
+ maintenance;
15
+ engineInfo;
16
+ epochs = [];
17
+ readToken = '';
18
+ inboxPending = 0;
7
19
  locks = new Map();
20
+ snapshot = emptySnapshot();
21
+ constructor(stateDir) {
22
+ this.filePath = join(stateDir, RUNTIME_STATE_FILE);
23
+ this.legacyPath = join(stateDir, LEGACY_JOURNAL_FILE);
24
+ }
8
25
  async exclusive(key, action) {
9
26
  const previous = this.locks.get(key) ?? Promise.resolve();
10
27
  let release;
@@ -22,93 +39,52 @@ export class RuntimeJournal {
22
39
  this.locks.delete(key);
23
40
  }
24
41
  }
25
- snapshot = {
26
- workspaces: {},
27
- workspaceFileRevisions: {},
28
- operations: {},
29
- receipts: {},
30
- events: {},
31
- sessions: {},
32
- terminals: {},
33
- checkpoints: {},
34
- fileRevisions: {},
35
- approvals: {},
36
- managedWrites: {},
37
- executionGroup: { stopConfirmed: false, replaced: false },
38
- lastSourceSequence: 0,
39
- acceptedLeaseFence: 0,
40
- destroyed: false,
41
- };
42
- constructor(stateDir) {
43
- this.filePath = join(stateDir, 'journal.json');
44
- }
45
42
  async load() {
46
- try {
47
- const parsed = JSON.parse(await readFile(this.filePath, 'utf8'));
48
- this.snapshot = {
49
- workspaces: Object.fromEntries(Object.entries(parsed.workspaces ?? {}).map(([key, value]) => [key, workspaceRegistrationSchema.parse(value)])),
50
- workspaceFileRevisions: parsed.workspaceFileRevisions ?? {},
51
- operations: Object.fromEntries(Object.entries(parsed.operations ?? {}).map(([key, value]) => [
52
- key,
53
- operationRecordSchema.parse(value),
54
- ])),
55
- receipts: Object.fromEntries(Object.entries(parsed.receipts ?? {}).map(([key, value]) => [
56
- key,
57
- sourceEventReceiptSchema.parse(value),
58
- ])),
59
- events: Object.fromEntries(Object.entries(parsed.events ?? {}).map(([key, value]) => [
60
- key,
61
- pendingSourceEventSchema.parse(value),
62
- ])),
63
- sessions: Object.fromEntries(Object.entries(parsed.sessions ?? {}).map(([key, value]) => [
64
- key,
65
- {
66
- ...value,
67
- recoverability: value.recoverability === 'history_only' ? 'history_only' : 'native',
68
- },
69
- ])),
70
- terminals: Object.fromEntries(Object.entries(parsed.terminals ?? {}).map(([key, value]) => [
71
- key,
72
- terminalRecordSchema.parse(value),
73
- ])),
74
- checkpoints: Object.fromEntries(Object.entries(parsed.checkpoints ?? {}).map(([key, value]) => [
75
- key,
76
- checkpointRecordSchema.parse(value),
77
- ])),
78
- fileRevisions: parsed.fileRevisions ?? {},
79
- managedWrites: sanitizeManagedWrites(parsed.managedWrites),
80
- fileCheckpointId: typeof parsed.fileCheckpointId === 'string' ? parsed.fileCheckpointId : undefined,
81
- consistentCheckpointId: typeof parsed.consistentCheckpointId === 'string' ? parsed.consistentCheckpointId : undefined,
82
- approvals: Object.fromEntries(Object.entries(parsed.approvals ?? {})
83
- .filter(([, value]) => isStoredApproval(value))
84
- .map(([key, value]) => [key, value])),
85
- executionGroup: {
86
- pid: integerOrUndefined(parsed.executionGroup?.pid),
87
- startedAt: typeof parsed.executionGroup?.startedAt === 'string'
88
- ? parsed.executionGroup.startedAt
89
- : undefined,
90
- previousPid: integerOrUndefined(parsed.executionGroup?.previousPid),
91
- stopConfirmed: parsed.executionGroup?.stopConfirmed === true,
92
- replaced: parsed.executionGroup?.replaced === true,
93
- },
94
- runtimeEpoch: integerOrUndefined(parsed.runtimeEpoch),
95
- lastSourceSequence: parsed.lastSourceSequence ?? 0,
96
- acceptedLeaseFence: parsed.acceptedLeaseFence ?? highestFence(parsed.operations ?? {}),
97
- destroyed: parsed.destroyed === true,
98
- };
99
- }
100
- catch (error) {
101
- if (error.code === 'ENOENT')
102
- return;
103
- throw error;
104
- }
43
+ await mkdir(dirname(this.filePath), { recursive: true, mode: 0o700 });
44
+ const legacy = await readLegacy(this.legacyPath, this.filePath);
45
+ const started = await SqliteWorkerClient.open(this.filePath, legacy?.value ?? null);
46
+ this.client = started.client;
47
+ this.apply(started.opened);
48
+ if (legacy?.readable)
49
+ await writeFile(this.legacyPath, LEGACY_JOURNAL_SENTINEL, { mode: 0o600 });
50
+ this.maintenance = setInterval(() => {
51
+ void this.client?.request('maintain').catch(() => undefined);
52
+ }, 30_000);
53
+ this.maintenance.unref?.();
54
+ }
55
+ async close() {
56
+ if (this.maintenance)
57
+ clearInterval(this.maintenance);
58
+ await this.client?.close();
59
+ this.client = undefined;
60
+ }
61
+ engine() {
62
+ if (!this.engineInfo)
63
+ throw new Error('sqlite_engine_unavailable');
64
+ return this.engineInfo;
65
+ }
66
+ sourceReadToken() {
67
+ return this.readToken;
68
+ }
69
+ pendingInbox() {
70
+ return this.inboxPending;
71
+ }
72
+ epochsSnapshot() {
73
+ return this.epochs.map((epoch) => ({ ...epoch }));
74
+ }
75
+ onSourceEvent(listener) {
76
+ this.listeners.add(listener);
77
+ return () => this.listeners.delete(listener);
105
78
  }
106
79
  getWorkspace(workspaceId) {
107
80
  return this.snapshot.workspaces[workspaceId];
108
81
  }
109
82
  async putWorkspace(binding) {
110
- this.snapshot.workspaces[binding.workspaceId] = workspaceRegistrationSchema.parse(binding);
111
- await this.flush();
83
+ const parsed = workspaceRegistrationSchema.parse(binding);
84
+ await this.run('upsertRecord', { bucket: 'workspaces', key: parsed.workspaceId, body: parsed });
85
+ }
86
+ async forgetWorkspaces() {
87
+ await this.run('forgetWorkspaces', {});
112
88
  }
113
89
  findSessionCreation(sessionId) {
114
90
  return Object.values(this.snapshot.operations).find((item) => item.kind === 'session.create' && item.sessionId === sessionId);
@@ -147,12 +123,10 @@ export class RuntimeJournal {
147
123
  return this.snapshot.consistentCheckpointId;
148
124
  }
149
125
  async setFileCheckpointId(checkpointId) {
150
- this.snapshot.fileCheckpointId = checkpointId;
151
- await this.flush();
126
+ await this.run('setCheckpointId', { field: 'file', id: checkpointId });
152
127
  }
153
128
  async setConsistentCheckpointId(checkpointId) {
154
- this.snapshot.consistentCheckpointId = checkpointId;
155
- await this.flush();
129
+ await this.run('setCheckpointId', { field: 'consistent', id: checkpointId });
156
130
  }
157
131
  listManagedWrites(workspaceId) {
158
132
  return Object.values(this.snapshot.managedWrites).filter((item) => !workspaceId || item.workspaceId === workspaceId);
@@ -161,8 +135,7 @@ export class RuntimeJournal {
161
135
  return this.snapshot.managedWrites[writeId];
162
136
  }
163
137
  async putManagedWrite(record) {
164
- this.snapshot.managedWrites[record.writeId] = record;
165
- await this.flush();
138
+ await this.run('upsertRecord', { bucket: 'managed_writes', key: record.writeId, body: record });
166
139
  }
167
140
  fileRevisions(workspaceId) {
168
141
  return { ...(workspaceId ? this.snapshot.workspaceFileRevisions[workspaceId] ?? {} : this.snapshot.fileRevisions) };
@@ -186,165 +159,205 @@ export class RuntimeJournal {
186
159
  return { ...this.snapshot.executionGroup };
187
160
  }
188
161
  getApproval(sessionId, approvalId) {
189
- return this.snapshot.approvals[approvalKey(sessionId, approvalId)];
162
+ return this.snapshot.approvals[`${sessionId}:${approvalId}`];
190
163
  }
191
164
  listApprovals() {
192
165
  return Object.values(this.snapshot.approvals);
193
166
  }
194
167
  async putApproval(approval) {
195
- this.snapshot.approvals[approvalKey(approval.sessionId, approval.approvalId)] = approval;
196
- await this.flush();
168
+ await this.run('upsertRecord', { bucket: 'approvals', key: `${approval.sessionId}:${approval.approvalId}`, body: approval });
197
169
  }
198
170
  async noteExecutionGroup(group) {
199
- this.snapshot.executionGroup = {
200
- ...this.snapshot.executionGroup,
201
- pid: group.pid,
202
- startedAt: group.startedAt,
203
- };
204
- await this.flush();
171
+ await this.run('noteExecutionGroup', { group });
205
172
  }
206
173
  async writeExecutionEpoch(input) {
207
- this.snapshot.runtimeEpoch = input.runtimeEpoch;
208
- this.snapshot.executionGroup = {
209
- pid: input.pid,
210
- startedAt: input.startedAt,
211
- previousPid: input.previousPid,
212
- stopConfirmed: input.stopConfirmed,
213
- replaced: true,
214
- };
215
- for (const [key, approval] of Object.entries(this.snapshot.approvals)) {
216
- if (approval.status === 'pending') {
217
- this.snapshot.approvals[key] = { ...approval, status: 'expired' };
218
- }
219
- }
220
- await this.flush();
174
+ await this.run('writeExecutionEpoch', input);
221
175
  }
222
176
  async setExecutionStopConfirmed(stopConfirmed) {
223
- this.snapshot.executionGroup = { ...this.snapshot.executionGroup, stopConfirmed };
224
- await this.flush();
177
+ await this.run('setStopConfirmed', { stopConfirmed });
178
+ }
179
+ async bindRuntime(input) {
180
+ await this.run('bindRuntime', input);
225
181
  }
226
182
  listEvents(afterSequence = 0) {
227
183
  return Object.values(this.snapshot.events)
228
184
  .filter((event) => event.source.sourceSequence > afterSequence)
185
+ .sort((left, right) => left.source.sourceSequence - right.source.sourceSequence || left.source.runtimeEpoch - right.source.runtimeEpoch);
186
+ }
187
+ readSourcePage(input) {
188
+ const epoch = this.epochs.find((item) => item.runtime_id === input.runtimeId && item.generation === input.generation && item.runtime_epoch === input.runtimeEpoch);
189
+ const retainedFrom = epoch?.retained_from ?? 1;
190
+ const highWater = epoch?.local_durable_through ?? 0;
191
+ const coveredThrough = input.after;
192
+ if (retainedRangeHasGap(retainedFrom, coveredThrough)) {
193
+ return { ok: false, code: 'source_gap', retainedFrom, highWater, coveredThrough, ...identity(input) };
194
+ }
195
+ const matched = Object.values(this.snapshot.events)
196
+ .filter((event) => event.source.runtimeId === input.runtimeId &&
197
+ event.source.generation === input.generation &&
198
+ event.source.runtimeEpoch === input.runtimeEpoch &&
199
+ event.source.sourceSequence > input.after)
229
200
  .sort((left, right) => left.source.sourceSequence - right.source.sourceSequence);
201
+ const first = matched[0];
202
+ if (first && first.source.sourceSequence > input.after + 1) {
203
+ return { ok: false, code: 'source_gap', retainedFrom, highWater, coveredThrough, ...identity(input) };
204
+ }
205
+ const events = matched.slice(0, input.limit ?? 100);
206
+ const nextAfter = events.at(-1)?.source.sourceSequence ?? input.after;
207
+ return { ok: true, events, retainedFrom, highWater, nextAfter, ...identity(input) };
208
+ }
209
+ async allocateSourceEvent(input) {
210
+ const result = await this.run('allocateEvent', input);
211
+ const event = result.committedEvents?.[0];
212
+ if (!event)
213
+ throw new Error('source_event_missing');
214
+ return event;
215
+ }
216
+ async commitSourceIdentity(event) {
217
+ await this.run('commitIdentity', { event: pendingSourceEventSchema.parse(event) });
218
+ }
219
+ async ingestNative(input) {
220
+ const result = await this.run('ingest', input);
221
+ return result.committedEvents ?? [];
222
+ }
223
+ async bindTurn(input) {
224
+ const result = await this.run('bindTurn', input);
225
+ return result.committedEvents ?? [];
226
+ }
227
+ async acknowledgeSource(ack) {
228
+ await this.run('ack', sourceAckSchema.parse(ack));
229
+ }
230
+ async createReplayPin(input) {
231
+ const result = await this.run('createPin', { identity: input, ttlMs: input.ttlMs });
232
+ return replayPinResultSchema.parse(result.pin);
233
+ }
234
+ async deleteReplayPin(pinId) {
235
+ await this.run('deletePin', { pinId });
236
+ }
237
+ async reclaimSourceEvents(minRetainMs = 0) {
238
+ await this.run('reclaim', { minRetainMs });
230
239
  }
231
240
  async putOperation(scopeKey, operation) {
232
- this.snapshot.operations[scopeKey] = operationRecordSchema.parse(operation);
233
- await this.flush();
234
- return this.snapshot.operations[scopeKey];
241
+ const parsed = operationRecordSchema.parse(operation);
242
+ await this.run('putOperation', { scopeKey, operation: parsed });
243
+ return this.snapshot.operations[scopeKey] ?? parsed;
235
244
  }
236
245
  async putSession(binding) {
237
- this.snapshot.sessions[binding.sessionId] = binding;
238
- await this.flush();
246
+ await this.run('upsertRecord', { bucket: 'sessions', key: binding.sessionId, body: binding });
239
247
  }
240
248
  async putTerminal(record) {
241
249
  const parsed = terminalRecordSchema.parse(record);
242
- this.snapshot.terminals[parsed.terminalId] = parsed;
243
- await this.flush();
250
+ await this.run('upsertRecord', { bucket: 'terminals', key: parsed.terminalId, body: parsed });
244
251
  return parsed;
245
252
  }
246
253
  async putCheckpoint(checkpoint) {
247
254
  const parsed = checkpointRecordSchema.parse(checkpoint);
248
- this.snapshot.checkpoints[parsed.checkpointId] = parsed;
249
- await this.flush();
255
+ await this.run('upsertRecord', { bucket: 'checkpoints', key: parsed.checkpointId, body: parsed });
250
256
  return parsed;
251
257
  }
252
258
  async putFileRevision(path, record, workspaceId) {
253
259
  if (workspaceId) {
254
- (this.snapshot.workspaceFileRevisions[workspaceId] ??= {})[path] = record;
255
- }
256
- else {
257
- this.snapshot.fileRevisions[path] = record;
260
+ await this.run('upsertRecord', { bucket: `workspace_files:${workspaceId}`, key: path, body: record });
261
+ return;
258
262
  }
259
- await this.flush();
263
+ await this.run('upsertRecord', { bucket: 'file_revisions', key: path, body: record });
260
264
  }
261
265
  async replaceFileRevisions(records, workspaceId) {
262
- if (workspaceId)
263
- this.snapshot.workspaceFileRevisions[workspaceId] = { ...records };
264
- else
265
- this.snapshot.fileRevisions = { ...records };
266
- await this.flush();
266
+ const entries = Object.entries(records).map(([key, body]) => ({ key, body }));
267
+ await this.run('replaceBucket', { bucket: workspaceId ? `workspace_files:${workspaceId}` : 'file_revisions', entries });
267
268
  }
268
269
  async markDestroyed() {
269
- this.snapshot.destroyed = true;
270
- await this.flush();
270
+ await this.run('setDestroyed', {});
271
271
  }
272
272
  async replaceSessions(sessions) {
273
- this.snapshot.sessions = Object.fromEntries(sessions.map((item) => [item.sessionId, item]));
274
- await this.flush();
273
+ await this.run('replaceBucket', {
274
+ bucket: 'sessions',
275
+ entries: sessions.map((session) => ({ key: session.sessionId, body: session })),
276
+ });
275
277
  }
276
278
  async setAcceptedLeaseFence(value) {
277
- this.snapshot.acceptedLeaseFence = Math.max(this.snapshot.acceptedLeaseFence, value);
278
- await this.flush();
279
+ await this.run('setAcceptedFence', { value });
279
280
  }
280
281
  async putReceipt(receipt, sourceSequence) {
281
- this.snapshot.receipts[sourceEventKey(receipt.source)] =
282
- sourceEventReceiptSchema.parse(receipt);
283
- this.snapshot.lastSourceSequence = Math.max(this.snapshot.lastSourceSequence, sourceSequence);
284
- await this.flush();
282
+ const parsed = sourceEventReceiptSchema.parse(receipt);
283
+ await this.run('upsertRecord', { bucket: 'receipts', key: sourceEventKey(parsed.source), body: parsed });
284
+ void sourceSequence;
285
285
  }
286
286
  async appendEvent(event) {
287
287
  const parsed = pendingSourceEventSchema.parse(event);
288
- this.snapshot.events[sourceEventKey(parsed.source)] = parsed;
289
- this.snapshot.lastSourceSequence = Math.max(this.snapshot.lastSourceSequence, parsed.source.sourceSequence);
290
- await this.flush();
291
- return parsed;
292
- }
293
- async flush() {
294
- const snapshot = `${JSON.stringify(this.snapshot)}\n`;
295
- const write = this.writes.then(async () => {
296
- await mkdir(dirname(this.filePath), { recursive: true, mode: 0o700 });
297
- const tempPath = `${this.filePath}.${process.pid}.tmp`;
298
- await writeFile(tempPath, snapshot, { mode: 0o600 });
299
- await rename(tempPath, this.filePath);
300
- });
301
- this.writes = write.catch(() => undefined);
302
- await write;
303
- }
304
- }
305
- function sanitizeManagedWrites(value) {
306
- if (!value || typeof value !== 'object')
307
- return {};
308
- const records = {};
309
- for (const [key, item] of Object.entries(value)) {
310
- if (!item || typeof item !== 'object')
311
- continue;
312
- const record = item;
313
- if (record.writeId !== key || typeof record.workspaceId !== 'string' || typeof record.startedAt !== 'string') {
314
- continue;
288
+ await this.commitSourceIdentity(parsed);
289
+ return this.snapshot.events[sourceEventKey(parsed.source)] ?? parsed;
290
+ }
291
+ async run(type, payload) {
292
+ if (!this.client)
293
+ throw new Error('sqlite_not_open');
294
+ const result = await this.client.request(type, payload);
295
+ this.apply(result);
296
+ for (const event of result.committedEvents ?? []) {
297
+ for (const listener of this.listeners)
298
+ listener(event);
315
299
  }
316
- if (record.source !== 'manual' && record.source !== 'agent' && record.source !== 'background')
317
- continue;
318
- records[key] = {
319
- writeId: key,
320
- workspaceId: record.workspaceId,
321
- source: record.source,
322
- startedAt: record.startedAt,
323
- pid: integerOrUndefined(record.pid),
324
- stoppedAt: typeof record.stoppedAt === 'string' ? record.stoppedAt : undefined,
300
+ return result;
301
+ }
302
+ apply(dump) {
303
+ const group = dump.snapshot.executionGroup;
304
+ this.snapshot = {
305
+ ...emptySnapshot(),
306
+ ...dump.snapshot,
307
+ executionGroup: {
308
+ stopConfirmed: group?.stopConfirmed === true,
309
+ replaced: group?.replaced === true,
310
+ pid: group?.pid,
311
+ startedAt: group?.startedAt,
312
+ previousPid: group?.previousPid,
313
+ },
325
314
  };
315
+ this.epochs = dump.epochs ?? [];
316
+ this.readToken = dump.sourceReadToken ?? this.readToken;
317
+ this.inboxPending = dump.pendingInbox ?? 0;
318
+ if (dump.engine)
319
+ this.engineInfo = dump.engine;
326
320
  }
327
- return records;
328
321
  }
329
- function approvalKey(sessionId, approvalId) {
330
- return `${sessionId}:${approvalId}`;
322
+ async function readLegacy(legacyPath, sqlitePath) {
323
+ if (!existsSync(legacyPath))
324
+ return null;
325
+ const text = await readFile(legacyPath, 'utf8');
326
+ if (text.startsWith(LEGACY_JOURNAL_SENTINEL.trim())) {
327
+ if (!existsSync(sqlitePath))
328
+ throw new Error('runtime_state_missing');
329
+ return null;
330
+ }
331
+ let value;
332
+ try {
333
+ value = JSON.parse(text);
334
+ }
335
+ catch {
336
+ throw new Error('legacy_journal_unreadable');
337
+ }
338
+ if (!existsSync(`${legacyPath}.bak`))
339
+ await copyFile(legacyPath, `${legacyPath}.bak`);
340
+ return { value, readable: true };
331
341
  }
332
- function integerOrUndefined(value) {
333
- return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined;
342
+ function identity(input) {
343
+ return { runtimeId: input.runtimeId, generation: input.generation, runtimeEpoch: input.runtimeEpoch };
334
344
  }
335
- function isStoredApproval(value) {
336
- if (!value || typeof value !== 'object')
337
- return false;
338
- const record = value;
339
- return (typeof record.approvalId === 'string' &&
340
- typeof record.sessionId === 'string' &&
341
- typeof record.runtimeEpoch === 'number' &&
342
- typeof record.nativeRequestId === 'string' &&
343
- (record.status === 'pending' ||
344
- record.status === 'approved' ||
345
- record.status === 'rejected' ||
346
- record.status === 'expired'));
347
- }
348
- function highestFence(operations) {
349
- return Object.values(operations).reduce((highest, operation) => Math.max(highest, operation.leaseFence ?? 0), 0);
345
+ function emptySnapshot() {
346
+ return {
347
+ workspaces: {},
348
+ workspaceFileRevisions: {},
349
+ operations: {},
350
+ receipts: {},
351
+ events: {},
352
+ sessions: {},
353
+ terminals: {},
354
+ checkpoints: {},
355
+ fileRevisions: {},
356
+ approvals: {},
357
+ managedWrites: {},
358
+ executionGroup: { stopConfirmed: false, replaced: false },
359
+ lastSourceSequence: 0,
360
+ acceptedLeaseFence: 0,
361
+ destroyed: false,
362
+ };
350
363
  }
@@ -0,0 +1,61 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { Worker } from 'node:worker_threads';
4
+ export class SqliteWorkerClient {
5
+ nextId = 1;
6
+ pending = new Map();
7
+ chain = Promise.resolve();
8
+ worker;
9
+ constructor(worker) {
10
+ this.worker = worker;
11
+ worker.on('message', (message) => {
12
+ const waiter = this.pending.get(message.id);
13
+ if (!waiter)
14
+ return;
15
+ this.pending.delete(message.id);
16
+ if (message.ok)
17
+ waiter.resolve(message.result);
18
+ else
19
+ waiter.reject(new Error(message.error || 'sqlite_worker_failed'));
20
+ });
21
+ worker.on('error', (error) => {
22
+ for (const waiter of this.pending.values())
23
+ waiter.reject(error);
24
+ this.pending.clear();
25
+ });
26
+ }
27
+ static async open(filePath, legacy) {
28
+ const client = new SqliteWorkerClient(new Worker(workerUrl(), { execArgv: [], env: { ...process.env, NODE_OPTIONS: '' } }));
29
+ try {
30
+ const opened = await client.request('open', { filePath, legacy });
31
+ return { client, opened };
32
+ }
33
+ catch (error) {
34
+ await client.close().catch(() => undefined);
35
+ throw error;
36
+ }
37
+ }
38
+ request(type, payload) {
39
+ const run = this.chain.then(() => this.send(type, payload));
40
+ this.chain = run.then(() => undefined, () => undefined);
41
+ return run;
42
+ }
43
+ async close() {
44
+ await this.request('close', {}).catch(() => undefined);
45
+ await this.worker.terminate();
46
+ }
47
+ send(type, payload) {
48
+ const id = this.nextId;
49
+ this.nextId += 1;
50
+ return new Promise((resolve, reject) => {
51
+ this.pending.set(id, { resolve: (value) => resolve(value), reject });
52
+ this.worker.postMessage({ id, type, payload });
53
+ });
54
+ }
55
+ }
56
+ function workerUrl() {
57
+ const compiled = new URL('./sqlite-worker.js', import.meta.url);
58
+ if (existsSync(fileURLToPath(compiled)))
59
+ return compiled;
60
+ return new URL('./sqlite-worker.ts', import.meta.url);
61
+ }