syncorejs 0.2.0 → 0.2.2

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.
Files changed (135) hide show
  1. package/README.md +2 -1
  2. package/dist/_vendor/cli/app.d.mts.map +1 -1
  3. package/dist/_vendor/cli/app.mjs +323 -42
  4. package/dist/_vendor/cli/app.mjs.map +1 -1
  5. package/dist/_vendor/cli/context.mjs +27 -9
  6. package/dist/_vendor/cli/context.mjs.map +1 -1
  7. package/dist/_vendor/cli/doctor.mjs +513 -46
  8. package/dist/_vendor/cli/doctor.mjs.map +1 -1
  9. package/dist/_vendor/cli/messages.mjs +5 -4
  10. package/dist/_vendor/cli/messages.mjs.map +1 -1
  11. package/dist/_vendor/cli/project.mjs +110 -12
  12. package/dist/_vendor/cli/project.mjs.map +1 -1
  13. package/dist/_vendor/cli/render.mjs +57 -9
  14. package/dist/_vendor/cli/render.mjs.map +1 -1
  15. package/dist/_vendor/cli/targets.mjs +4 -3
  16. package/dist/_vendor/cli/targets.mjs.map +1 -1
  17. package/dist/_vendor/core/cli.d.mts +13 -3
  18. package/dist/_vendor/core/cli.d.mts.map +1 -1
  19. package/dist/_vendor/core/cli.mjs +242 -91
  20. package/dist/_vendor/core/cli.mjs.map +1 -1
  21. package/dist/_vendor/core/devtools-auth.mjs +60 -0
  22. package/dist/_vendor/core/devtools-auth.mjs.map +1 -0
  23. package/dist/_vendor/core/index.d.mts +5 -3
  24. package/dist/_vendor/core/index.mjs +22 -2
  25. package/dist/_vendor/core/index.mjs.map +1 -1
  26. package/dist/_vendor/core/runtime/components.d.mts +111 -0
  27. package/dist/_vendor/core/runtime/components.d.mts.map +1 -0
  28. package/dist/_vendor/core/runtime/components.mjs +186 -0
  29. package/dist/_vendor/core/runtime/components.mjs.map +1 -0
  30. package/dist/_vendor/core/runtime/devtools.d.mts +4 -4
  31. package/dist/_vendor/core/runtime/devtools.d.mts.map +1 -1
  32. package/dist/_vendor/core/runtime/devtools.mjs +52 -41
  33. package/dist/_vendor/core/runtime/devtools.mjs.map +1 -1
  34. package/dist/_vendor/core/runtime/functions.d.mts +10 -10
  35. package/dist/_vendor/core/runtime/functions.d.mts.map +1 -1
  36. package/dist/_vendor/core/runtime/functions.mjs +2 -2
  37. package/dist/_vendor/core/runtime/functions.mjs.map +1 -1
  38. package/dist/_vendor/core/runtime/internal/engines/devtoolsEngine.mjs +77 -0
  39. package/dist/_vendor/core/runtime/internal/engines/devtoolsEngine.mjs.map +1 -0
  40. package/dist/_vendor/core/runtime/internal/engines/executionEngine.mjs +617 -0
  41. package/dist/_vendor/core/runtime/internal/engines/executionEngine.mjs.map +1 -0
  42. package/dist/_vendor/core/runtime/internal/engines/reactivityEngine.mjs +186 -0
  43. package/dist/_vendor/core/runtime/internal/engines/reactivityEngine.mjs.map +1 -0
  44. package/dist/_vendor/core/runtime/internal/engines/schedulerEngine.mjs +220 -0
  45. package/dist/_vendor/core/runtime/internal/engines/schedulerEngine.mjs.map +1 -0
  46. package/dist/_vendor/core/runtime/internal/engines/schemaEngine.mjs +203 -0
  47. package/dist/_vendor/core/runtime/internal/engines/schemaEngine.mjs.map +1 -0
  48. package/dist/_vendor/core/runtime/internal/engines/shared.mjs +177 -0
  49. package/dist/_vendor/core/runtime/internal/engines/shared.mjs.map +1 -0
  50. package/dist/_vendor/core/runtime/internal/engines/storageEngine.mjs +144 -0
  51. package/dist/_vendor/core/runtime/internal/engines/storageEngine.mjs.map +1 -0
  52. package/dist/_vendor/core/runtime/internal/runtimeKernel.mjs +220 -0
  53. package/dist/_vendor/core/runtime/internal/runtimeKernel.mjs.map +1 -0
  54. package/dist/_vendor/core/runtime/internal/runtimeStatus.mjs +32 -0
  55. package/dist/_vendor/core/runtime/internal/runtimeStatus.mjs.map +1 -0
  56. package/dist/_vendor/core/runtime/internal/systemMeta.mjs +61 -0
  57. package/dist/_vendor/core/runtime/internal/systemMeta.mjs.map +1 -0
  58. package/dist/_vendor/core/runtime/internal/transactionCoordinator.mjs +37 -0
  59. package/dist/_vendor/core/runtime/internal/transactionCoordinator.mjs.map +1 -0
  60. package/dist/_vendor/core/runtime/runtime.d.mts +159 -205
  61. package/dist/_vendor/core/runtime/runtime.d.mts.map +1 -1
  62. package/dist/_vendor/core/runtime/runtime.mjs +16 -1371
  63. package/dist/_vendor/core/runtime/runtime.mjs.map +1 -1
  64. package/dist/_vendor/core/transport.d.mts +111 -0
  65. package/dist/_vendor/core/transport.d.mts.map +1 -0
  66. package/dist/_vendor/core/transport.mjs +419 -0
  67. package/dist/_vendor/core/transport.mjs.map +1 -0
  68. package/dist/_vendor/devtools-protocol/index.d.ts +39 -1
  69. package/dist/_vendor/devtools-protocol/index.d.ts.map +1 -1
  70. package/dist/_vendor/devtools-protocol/index.js +25 -9
  71. package/dist/_vendor/devtools-protocol/index.js.map +1 -1
  72. package/dist/_vendor/next/index.d.ts +1 -1
  73. package/dist/_vendor/next/index.d.ts.map +1 -1
  74. package/dist/_vendor/next/index.js +31 -13
  75. package/dist/_vendor/next/index.js.map +1 -1
  76. package/dist/_vendor/platform-expo/index.d.ts +12 -12
  77. package/dist/_vendor/platform-expo/index.d.ts.map +1 -1
  78. package/dist/_vendor/platform-expo/index.js +4 -2
  79. package/dist/_vendor/platform-expo/index.js.map +1 -1
  80. package/dist/_vendor/platform-expo/react.d.ts.map +1 -1
  81. package/dist/_vendor/platform-expo/react.js +11 -10
  82. package/dist/_vendor/platform-expo/react.js.map +1 -1
  83. package/dist/_vendor/platform-node/index.d.mts +23 -19
  84. package/dist/_vendor/platform-node/index.d.mts.map +1 -1
  85. package/dist/_vendor/platform-node/index.mjs +13 -5
  86. package/dist/_vendor/platform-node/index.mjs.map +1 -1
  87. package/dist/_vendor/platform-node/ipc-react.d.mts.map +1 -1
  88. package/dist/_vendor/platform-node/ipc-react.mjs +15 -2
  89. package/dist/_vendor/platform-node/ipc-react.mjs.map +1 -1
  90. package/dist/_vendor/platform-node/ipc.d.mts +11 -35
  91. package/dist/_vendor/platform-node/ipc.d.mts.map +1 -1
  92. package/dist/_vendor/platform-node/ipc.mjs +3 -273
  93. package/dist/_vendor/platform-node/ipc.mjs.map +1 -1
  94. package/dist/_vendor/platform-web/external-change.d.ts +2 -1
  95. package/dist/_vendor/platform-web/external-change.d.ts.map +1 -1
  96. package/dist/_vendor/platform-web/external-change.js +2 -1
  97. package/dist/_vendor/platform-web/external-change.js.map +1 -1
  98. package/dist/_vendor/platform-web/index.d.ts +21 -21
  99. package/dist/_vendor/platform-web/index.d.ts.map +1 -1
  100. package/dist/_vendor/platform-web/index.js +44 -7
  101. package/dist/_vendor/platform-web/index.js.map +1 -1
  102. package/dist/_vendor/platform-web/react.d.ts.map +1 -1
  103. package/dist/_vendor/platform-web/react.js +29 -13
  104. package/dist/_vendor/platform-web/react.js.map +1 -1
  105. package/dist/_vendor/platform-web/worker.d.ts +11 -35
  106. package/dist/_vendor/platform-web/worker.d.ts.map +1 -1
  107. package/dist/_vendor/platform-web/worker.js +3 -267
  108. package/dist/_vendor/platform-web/worker.js.map +1 -1
  109. package/dist/_vendor/react/index.d.ts +36 -20
  110. package/dist/_vendor/react/index.d.ts.map +1 -1
  111. package/dist/_vendor/react/index.js +279 -57
  112. package/dist/_vendor/react/index.js.map +1 -1
  113. package/dist/_vendor/schema/definition.d.ts +48 -63
  114. package/dist/_vendor/schema/definition.d.ts.map +1 -1
  115. package/dist/_vendor/schema/definition.js +22 -39
  116. package/dist/_vendor/schema/definition.js.map +1 -1
  117. package/dist/_vendor/schema/index.d.ts +4 -4
  118. package/dist/_vendor/schema/index.js +2 -2
  119. package/dist/_vendor/schema/planner.d.ts +19 -2
  120. package/dist/_vendor/schema/planner.d.ts.map +1 -1
  121. package/dist/_vendor/schema/planner.js +79 -3
  122. package/dist/_vendor/schema/planner.js.map +1 -1
  123. package/dist/_vendor/schema/validators.d.ts +141 -121
  124. package/dist/_vendor/schema/validators.d.ts.map +1 -1
  125. package/dist/_vendor/schema/validators.js +300 -42
  126. package/dist/_vendor/schema/validators.js.map +1 -1
  127. package/dist/_vendor/svelte/index.d.ts +47 -19
  128. package/dist/_vendor/svelte/index.d.ts.map +1 -1
  129. package/dist/_vendor/svelte/index.js +250 -20
  130. package/dist/_vendor/svelte/index.js.map +1 -1
  131. package/dist/components.d.ts +2 -0
  132. package/dist/components.js +2 -0
  133. package/dist/index.d.ts +3 -2
  134. package/dist/index.js +2 -1
  135. package/package.json +8 -3
@@ -1,1381 +1,42 @@
1
- import { generateId } from "./id.mjs";
2
- import { createSchemaSnapshot, describeValidator, diffSchemaSnapshots, parseSchemaSnapshot, renderCreateSearchIndexStatement, renderMigrationSql, searchIndexTableName } from "../../schema/index.js";
3
- import { fromZonedTime, toZonedTime } from "date-fns-tz";
1
+ import { RuntimeKernel } from "./internal/runtimeKernel.mjs";
4
2
  //#region src/runtime/runtime.ts
5
- const DEFAULT_MISFIRE_POLICY = { type: "catch_up" };
6
- var RuntimeFilterBuilder = class {
7
- eq(field, value) {
8
- return {
9
- type: "condition",
10
- condition: {
11
- field,
12
- operator: "=",
13
- value
14
- }
15
- };
16
- }
17
- gt(field, value) {
18
- return {
19
- type: "condition",
20
- condition: {
21
- field,
22
- operator: ">",
23
- value
24
- }
25
- };
26
- }
27
- gte(field, value) {
28
- return {
29
- type: "condition",
30
- condition: {
31
- field,
32
- operator: ">=",
33
- value
34
- }
35
- };
36
- }
37
- lt(field, value) {
38
- return {
39
- type: "condition",
40
- condition: {
41
- field,
42
- operator: "<",
43
- value
44
- }
45
- };
46
- }
47
- lte(field, value) {
48
- return {
49
- type: "condition",
50
- condition: {
51
- field,
52
- operator: "<=",
53
- value
54
- }
55
- };
56
- }
57
- and(...expressions) {
58
- return {
59
- type: "and",
60
- expressions
61
- };
62
- }
63
- or(...expressions) {
64
- return {
65
- type: "or",
66
- expressions
67
- };
68
- }
69
- };
70
- var RuntimeIndexRangeBuilder = class {
71
- conditions = [];
72
- eq(field, value) {
73
- this.conditions.push({
74
- field,
75
- operator: "=",
76
- value
77
- });
78
- return this;
79
- }
80
- gt(field, value) {
81
- this.conditions.push({
82
- field,
83
- operator: ">",
84
- value
85
- });
86
- return this;
87
- }
88
- gte(field, value) {
89
- this.conditions.push({
90
- field,
91
- operator: ">=",
92
- value
93
- });
94
- return this;
95
- }
96
- lt(field, value) {
97
- this.conditions.push({
98
- field,
99
- operator: "<",
100
- value
101
- });
102
- return this;
103
- }
104
- lte(field, value) {
105
- this.conditions.push({
106
- field,
107
- operator: "<=",
108
- value
109
- });
110
- return this;
111
- }
112
- build() {
113
- return [...this.conditions];
114
- }
115
- };
116
- var RuntimeSearchIndexBuilder = class {
117
- searchField;
118
- searchText;
119
- filters = [];
120
- search(field, value) {
121
- this.searchField = field;
122
- this.searchText = value;
123
- return this;
124
- }
125
- eq(field, value) {
126
- this.filters.push({
127
- field,
128
- operator: "=",
129
- value
130
- });
131
- return this;
132
- }
133
- build() {
134
- if (!this.searchField || !this.searchText) throw new Error("Search queries require a search field and search text.");
135
- return {
136
- searchField: this.searchField,
137
- searchText: this.searchText,
138
- filters: [...this.filters]
139
- };
140
- }
141
- };
142
- var RuntimeQueryBuilder = class {
143
- orderDirection = "asc";
144
- source = { type: "table" };
145
- filterExpression;
146
- constructor(executeQuery, tableName, dependencyCollector) {
147
- this.executeQuery = executeQuery;
148
- this.tableName = tableName;
149
- this.dependencyCollector = dependencyCollector;
150
- }
151
- withIndex(indexName, builder) {
152
- this.source = {
153
- type: "index",
154
- name: indexName,
155
- range: builder?.(new RuntimeIndexRangeBuilder()).build() ?? []
156
- };
157
- return this;
158
- }
159
- withSearchIndex(indexName, builder) {
160
- this.source = {
161
- type: "search",
162
- name: indexName,
163
- query: builder(new RuntimeSearchIndexBuilder()).build()
164
- };
165
- return this;
166
- }
167
- order(order) {
168
- this.orderDirection = order;
169
- return this;
170
- }
171
- filter(builder) {
172
- this.filterExpression = builder(new RuntimeFilterBuilder());
173
- return this;
174
- }
175
- async collect() {
176
- return this.execute();
177
- }
178
- async take(count) {
179
- return this.execute({ limit: count });
180
- }
181
- async first() {
182
- return (await this.execute({ limit: 1 }))[0] ?? null;
183
- }
184
- async unique() {
185
- const results = await this.execute({ limit: 2 });
186
- if (results.length > 1) throw new Error("Expected a unique result but found multiple rows.");
187
- return results[0] ?? null;
188
- }
189
- async paginate(options) {
190
- const offset = options.cursor ? Number.parseInt(options.cursor, 10) : 0;
191
- const page = await this.execute({
192
- limit: options.numItems,
193
- offset
194
- });
195
- const nextCursor = page.length < options.numItems ? null : String(offset + page.length);
196
- return {
197
- page,
198
- cursor: nextCursor,
199
- isDone: nextCursor === null
200
- };
201
- }
202
- async execute(options) {
203
- this.dependencyCollector?.add(`table:${this.tableName}`);
204
- const queryOptions = {
205
- tableName: this.tableName,
206
- source: this.source,
207
- filterExpression: this.filterExpression,
208
- orderDirection: this.orderDirection
209
- };
210
- if (this.dependencyCollector) queryOptions.dependencyCollector = this.dependencyCollector;
211
- if (options?.limit !== void 0) queryOptions.limit = options.limit;
212
- if (options?.offset !== void 0) queryOptions.offset = options.offset;
213
- return this.executeQuery(queryOptions);
214
- }
215
- };
216
3
  /**
217
- * The local Syncore runtime that owns the database, storage, scheduler, and function execution.
4
+ * Local-first Syncore runtime that hosts your schema, functions, and storage.
218
5
  */
219
6
  var SyncoreRuntime = class {
220
- runtimeId = generateId();
221
- platform;
222
- capabilities;
223
- experimentalPlugins;
224
- activeQueries = /* @__PURE__ */ new Map();
225
- disabledSearchIndexes = /* @__PURE__ */ new Set();
226
- recentEvents = [];
227
- devtoolsListeners = /* @__PURE__ */ new Set();
228
- devtoolsInvalidationListeners = /* @__PURE__ */ new Set();
229
- externalChangeSourceId = generateId();
230
- detachExternalChangeListener;
231
- pendingExternalChangePromise;
232
- queuedExternalChange;
233
- schedulerTimer;
234
- recurringJobs;
235
- schedulerPollIntervalMs;
236
- driverDatabasePath;
237
- prepared = false;
238
- started = false;
7
+ kernel;
239
8
  constructor(options) {
240
9
  this.options = options;
241
- this.platform = options.platform ?? "node";
242
- this.experimentalPlugins = options.experimentalPlugins ?? [];
243
- this.recurringJobs = options.scheduler?.recurringJobs ?? [];
244
- this.schedulerPollIntervalMs = options.scheduler?.pollIntervalMs ?? 1e3;
245
- this.capabilities = Object.freeze(this.buildCapabilities());
246
- this.driverDatabasePath = inferDriverDatabasePath(options.driver);
247
- this.options.devtools?.attachRuntime?.(this);
10
+ this.kernel = new RuntimeKernel(options, this);
248
11
  }
249
- /**
250
- * Start the local Syncore runtime.
251
- */
252
12
  async start() {
253
- if (this.started) return;
254
- await this.prepareForDirectAccess();
255
- await this.runPluginHook("onStart");
256
- this.detachExternalChangeListener = this.options.externalChangeSignal?.subscribe((event) => {
257
- this.handleExternalChangeEvent(event);
258
- });
259
- this.schedulerTimer = setInterval(() => {
260
- this.processDueJobs();
261
- }, this.schedulerPollIntervalMs);
262
- this.started = true;
263
- this.emitDevtools({
264
- type: "runtime.connected",
265
- runtimeId: this.runtimeId,
266
- platform: this.platform,
267
- timestamp: Date.now()
268
- });
13
+ await this.kernel.start();
269
14
  }
270
15
  async prepareForDirectAccess() {
271
- if (this.prepared) return;
272
- await this.ensureSystemTables();
273
- await this.reconcileStorageState();
274
- await this.applySchema();
275
- await this.syncRecurringJobs();
276
- this.prepared = true;
16
+ await this.kernel.prepareForDirectAccess();
277
17
  }
278
- /**
279
- * Stop the local Syncore runtime and release any open resources.
280
- */
281
18
  async stop() {
282
- if (this.schedulerTimer) {
283
- clearInterval(this.schedulerTimer);
284
- this.schedulerTimer = void 0;
285
- }
286
- if (this.started) await this.runPluginHook("onStop");
287
- this.detachExternalChangeListener?.();
288
- this.detachExternalChangeListener = void 0;
289
- await this.options.driver.close?.();
290
- if (this.started) this.emitDevtools({
291
- type: "runtime.disconnected",
292
- runtimeId: this.runtimeId,
293
- timestamp: Date.now()
294
- });
295
- this.started = false;
19
+ await this.kernel.stop();
296
20
  }
297
- /**
298
- * Create a typed client for calling this runtime from the same process.
299
- */
300
21
  createClient() {
301
- return {
302
- query: (reference, ...args) => this.runQuery(reference, normalizeOptionalArgs(args)),
303
- mutation: (reference, ...args) => this.runMutation(reference, normalizeOptionalArgs(args)),
304
- action: (reference, ...args) => this.runAction(reference, normalizeOptionalArgs(args)),
305
- watchQuery: (reference, ...args) => this.watchQuery(reference, normalizeOptionalArgs(args))
306
- };
307
- }
308
- async getDevtoolsLiveQuerySnapshot() {
309
- return {
310
- summary: this.getRuntimeSummary(),
311
- activeQueries: this.getActiveQueryInfos(),
312
- schemaTables: await this.getSchemaTablesForDevtools()
313
- };
314
- }
315
- getRuntimeSummary() {
316
- return {
317
- runtimeId: this.runtimeId,
318
- platform: this.platform,
319
- connectedAt: Date.now(),
320
- activeQueryCount: this.activeQueries.size,
321
- recentEventCount: this.recentEvents.length
322
- };
323
- }
324
- getActiveQueryInfos() {
325
- return [...this.activeQueries.values()].map((query) => ({
326
- id: query.id,
327
- functionName: query.functionName,
328
- dependencyKeys: [...query.dependencyKeys],
329
- lastRunAt: query.lastRunAt
330
- }));
331
- }
332
- getDriverDatabasePath() {
333
- return this.driverDatabasePath;
334
- }
335
- async cancelScheduledJob(id) {
336
- await this.prepareForDirectAccess();
337
- if (((await this.options.driver.run(`UPDATE "_scheduled_functions"
338
- SET status = 'cancelled', updated_at = ?
339
- WHERE id = ? AND status = 'scheduled'`, [Date.now(), id])).changes ?? 0) > 0) {
340
- this.notifySchedulerJobsChanged();
341
- return true;
342
- }
343
- return false;
344
- }
345
- async updateScheduledJob(options) {
346
- await this.prepareForDirectAccess();
347
- const existing = await this.options.driver.get(`SELECT status, recurring_name FROM "_scheduled_functions" WHERE id = ?`, [options.id]);
348
- if (!existing || existing.status !== "scheduled" || !existing.recurring_name) return false;
349
- const now = Date.now();
350
- const runAt = options.runAt ?? computeNextRun(options.schedule, now);
351
- if (((await this.options.driver.run(`UPDATE "_scheduled_functions"
352
- SET args_json = ?, run_at = ?, updated_at = ?, schedule_json = ?, timezone = ?, misfire_policy = ?, window_ms = ?
353
- WHERE id = ? AND status = 'scheduled' AND recurring_name IS NOT NULL`, [
354
- stableStringify(options.args),
355
- runAt,
356
- now,
357
- stableStringify(options.schedule),
358
- "timezone" in options.schedule ? options.schedule.timezone ?? null : null,
359
- options.misfirePolicy.type,
360
- options.misfirePolicy.type === "windowed" ? options.misfirePolicy.windowMs : null,
361
- options.id
362
- ])).changes ?? 0) > 0) {
363
- this.notifySchedulerJobsChanged();
364
- return true;
365
- }
366
- return false;
367
- }
368
- subscribeToDevtoolsEvents(listener) {
369
- this.devtoolsListeners.add(listener);
370
- return () => {
371
- this.devtoolsListeners.delete(listener);
372
- };
373
- }
374
- subscribeToDevtoolsInvalidations(listener) {
375
- this.devtoolsInvalidationListeners.add(listener);
376
- return () => {
377
- this.devtoolsInvalidationListeners.delete(listener);
378
- };
22
+ return this.kernel.createClient();
379
23
  }
380
- notifyDevtoolsScopes(scopes) {
381
- const scopeSet = new Set(scopes);
382
- if (scopeSet.size === 0) return;
383
- for (const listener of this.devtoolsInvalidationListeners) listener(scopeSet);
384
- }
385
- async runDevtoolsMutation(callback, meta = {}) {
386
- const mutationId = generateId();
387
- const startedAt = Date.now();
388
- const changedTables = /* @__PURE__ */ new Set();
389
- const storageChanges = [];
390
- const result = await this.options.driver.withTransaction(async () => callback({ db: this.createDatabaseWriter({
391
- mutationDepth: 1,
392
- changedTables,
393
- storageChanges
394
- }) }));
395
- await this.refreshInvalidatedQueries(changedTables, mutationId);
396
- if (storageChanges.length > 0) await this.refreshAllActiveQueries();
397
- if (changedTables.size > 0) await this.publishExternalChange({
398
- scope: "database",
399
- reason: "commit",
400
- changedTables: [...changedTables]
401
- });
402
- await this.publishStorageChanges(storageChanges);
403
- this.emitDevtools({
404
- type: "mutation.committed",
405
- runtimeId: this.runtimeId,
406
- mutationId,
407
- functionName: "__devtools__/mutation",
408
- changedTables: [...changedTables],
409
- durationMs: Date.now() - startedAt,
410
- timestamp: Date.now(),
411
- ...meta.origin ? { origin: meta.origin } : {}
412
- });
413
- return result;
414
- }
415
- async forceRefreshDevtools(reason, meta = {}) {
416
- await this.refreshAllActiveQueries();
417
- await this.publishExternalChange({
418
- scope: "database",
419
- reason: "reconcile"
420
- });
421
- this.notifyDevtoolsScopes(["all"]);
422
- this.emitDevtools({
423
- type: "log",
424
- runtimeId: this.runtimeId,
425
- level: "info",
426
- message: reason,
427
- timestamp: Date.now(),
428
- ...meta.origin ? { origin: meta.origin } : {}
429
- });
430
- }
431
- getRuntimeId() {
432
- return this.runtimeId;
24
+ getAdmin() {
25
+ return this.kernel.admin;
433
26
  }
434
27
  async runQuery(reference, args = {}, meta = {}) {
435
- const definition = this.resolveFunction(reference, "query");
436
- const dependencyCollector = /* @__PURE__ */ new Set();
437
- const startedAt = Date.now();
438
- const result = await this.invokeFunction(definition, args, {
439
- mutationDepth: 0,
440
- changedTables: /* @__PURE__ */ new Set(),
441
- storageChanges: [],
442
- dependencyCollector
443
- });
444
- this.emitDevtools({
445
- type: "query.executed",
446
- runtimeId: this.runtimeId,
447
- queryId: reference.name,
448
- functionName: reference.name,
449
- dependencies: [...dependencyCollector],
450
- durationMs: Date.now() - startedAt,
451
- timestamp: Date.now(),
452
- ...meta.origin ? { origin: meta.origin } : {}
453
- });
454
- return result;
28
+ return this.kernel.executionEngine.runQuery(reference, args, meta);
455
29
  }
456
30
  async runMutation(reference, args = {}, meta = {}) {
457
- const definition = this.resolveFunction(reference, "mutation");
458
- const mutationId = generateId();
459
- const startedAt = Date.now();
460
- const changedTables = /* @__PURE__ */ new Set();
461
- const storageChanges = [];
462
- const result = await this.options.driver.withTransaction(async () => this.invokeFunction(definition, args, {
463
- mutationDepth: 1,
464
- changedTables,
465
- storageChanges
466
- }));
467
- await this.refreshInvalidatedQueries(changedTables, mutationId);
468
- if (storageChanges.length > 0) await this.refreshAllActiveQueries();
469
- if (changedTables.size > 0) await this.publishExternalChange({
470
- scope: "database",
471
- reason: "commit",
472
- changedTables: [...changedTables]
473
- });
474
- await this.publishStorageChanges(storageChanges);
475
- this.emitDevtools({
476
- type: "mutation.committed",
477
- runtimeId: this.runtimeId,
478
- mutationId,
479
- functionName: reference.name,
480
- changedTables: [...changedTables],
481
- durationMs: Date.now() - startedAt,
482
- timestamp: Date.now(),
483
- ...meta.origin ? { origin: meta.origin } : {}
484
- });
485
- return result;
31
+ return this.kernel.executionEngine.runMutation(reference, args, meta);
486
32
  }
487
33
  async runAction(reference, args = {}, meta = {}) {
488
- const definition = this.resolveFunction(reference, "action");
489
- const actionId = generateId();
490
- const startedAt = Date.now();
491
- try {
492
- const result = await this.invokeFunction(definition, args, {
493
- mutationDepth: 0,
494
- changedTables: /* @__PURE__ */ new Set(),
495
- storageChanges: []
496
- });
497
- this.emitDevtools({
498
- type: "action.completed",
499
- runtimeId: this.runtimeId,
500
- actionId,
501
- functionName: reference.name,
502
- durationMs: Date.now() - startedAt,
503
- timestamp: Date.now(),
504
- ...meta.origin ? { origin: meta.origin } : {}
505
- });
506
- return result;
507
- } catch (error) {
508
- this.emitDevtools({
509
- type: "action.completed",
510
- runtimeId: this.runtimeId,
511
- actionId,
512
- functionName: reference.name,
513
- durationMs: Date.now() - startedAt,
514
- timestamp: Date.now(),
515
- ...meta.origin ? { origin: meta.origin } : {},
516
- error: error instanceof Error ? error.message : String(error)
517
- });
518
- throw error;
519
- }
34
+ return this.kernel.executionEngine.runAction(reference, args, meta);
520
35
  }
521
36
  watchQuery(reference, args = {}) {
522
- const key = this.createActiveQueryKey(reference.name, args);
523
- let record = this.activeQueries.get(key);
524
- if (!record) {
525
- record = {
526
- id: key,
527
- functionName: reference.name,
528
- args,
529
- listeners: /* @__PURE__ */ new Set(),
530
- consumers: 0,
531
- dependencyKeys: /* @__PURE__ */ new Set(),
532
- lastResult: void 0,
533
- lastError: void 0,
534
- lastRunAt: 0
535
- };
536
- this.activeQueries.set(key, record);
537
- this.rerunActiveQuery(record);
538
- }
539
- const activeRecord = record;
540
- activeRecord.consumers += 1;
541
- let disposed = false;
542
- const ownedListeners = /* @__PURE__ */ new Set();
543
- return {
544
- onUpdate: (callback) => {
545
- activeRecord.listeners.add(callback);
546
- ownedListeners.add(callback);
547
- queueMicrotask(callback);
548
- return () => {
549
- activeRecord.listeners.delete(callback);
550
- ownedListeners.delete(callback);
551
- };
552
- },
553
- localQueryResult: () => activeRecord.lastResult,
554
- localQueryError: () => activeRecord.lastError,
555
- dispose: () => {
556
- if (disposed) return;
557
- disposed = true;
558
- for (const callback of ownedListeners) activeRecord.listeners.delete(callback);
559
- ownedListeners.clear();
560
- activeRecord.consumers = Math.max(0, activeRecord.consumers - 1);
561
- if (activeRecord.consumers === 0) this.activeQueries.delete(key);
562
- }
563
- };
564
- }
565
- async executeQueryBuilder(options) {
566
- const table = this.options.schema.getTable(options.tableName);
567
- const params = [];
568
- const whereClauses = [];
569
- const orderClauses = [];
570
- let joinClause = "";
571
- const source = options.source;
572
- if (source.type === "index") {
573
- const index = table.indexes.find((candidate) => candidate.name === source.name);
574
- if (!index) throw new Error(`Unknown index "${source.name}" on table "${options.tableName}".`);
575
- for (const condition of source.range) whereClauses.push(this.renderCondition("t", condition, params));
576
- const primaryField = index.fields[0];
577
- if (primaryField) orderClauses.push(`${fieldExpression("t", primaryField)} ${options.orderDirection.toUpperCase()}`);
578
- }
579
- if (source.type === "search") {
580
- const searchIndex = table.searchIndexes.find((candidate) => candidate.name === source.name);
581
- if (!searchIndex) throw new Error(`Unknown search index "${source.name}" on table "${options.tableName}".`);
582
- if (searchIndex.searchField !== source.query.searchField) throw new Error(`Search index "${searchIndex.name}" expects field "${searchIndex.searchField}".`);
583
- const searchIndexKey = `${options.tableName}:${searchIndex.name}`;
584
- if (this.disabledSearchIndexes.has(searchIndexKey)) {
585
- whereClauses.push(`${fieldExpression("t", searchIndex.searchField)} LIKE ?`);
586
- params.push(`%${source.query.searchText}%`);
587
- } else {
588
- joinClause = `JOIN ${quoteIdentifier(searchIndexTableName(options.tableName, searchIndex.name))} s ON s._id = t._id`;
589
- whereClauses.push(`s.search_value MATCH ?`);
590
- params.push(source.query.searchText);
591
- }
592
- for (const condition of source.query.filters) whereClauses.push(this.renderCondition("t", condition, params));
593
- }
594
- if (options.filterExpression) whereClauses.push(this.renderExpression("t", options.filterExpression, params));
595
- if (orderClauses.length === 0) orderClauses.push(`t._creationTime ${options.orderDirection.toUpperCase()}`);
596
- orderClauses.push(`t._id ${options.orderDirection.toUpperCase()}`);
597
- const sql = [
598
- `SELECT t._id, t._creationTime, t._json FROM ${quoteIdentifier(options.tableName)} t`,
599
- joinClause,
600
- whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "",
601
- `ORDER BY ${orderClauses.join(", ")}`,
602
- options.limit !== void 0 ? `LIMIT ${options.limit}` : "",
603
- options.offset !== void 0 ? `OFFSET ${options.offset}` : ""
604
- ].filter(Boolean).join(" ");
605
- return (await this.options.driver.all(sql, params)).map((row) => this.deserializeDocument(options.tableName, row));
606
- }
607
- async invokeFunction(definition, rawArgs, state) {
608
- const args = definition.argsValidator.parse(rawArgs);
609
- const ctx = this.createContext(definition.kind, state);
610
- const result = await definition.handler(ctx, args);
611
- if (definition.returnsValidator) return definition.returnsValidator.parse(result);
612
- return result;
613
- }
614
- createContext(kind, state) {
615
- const db = kind === "mutation" ? this.createDatabaseWriter(state) : this.createDatabaseReader(state);
616
- const storage = this.createStorageApi(state);
617
- const scheduler = this.createSchedulerApi();
618
- return {
619
- db,
620
- storage,
621
- capabilities: this.capabilities,
622
- scheduler,
623
- runQuery: (reference, ...args) => this.runQuery(reference, normalizeOptionalArgs(args)),
624
- runMutation: (reference, ...args) => {
625
- const normalizedArgs = normalizeOptionalArgs(args);
626
- if (kind === "mutation") return this.options.driver.withSavepoint(`sp_${generateId().replace(/-/g, "_")}`, () => this.invokeFunction(this.resolveFunction(reference, "mutation"), normalizedArgs, {
627
- mutationDepth: state.mutationDepth + 1,
628
- changedTables: state.changedTables,
629
- storageChanges: state.storageChanges
630
- }));
631
- return this.runMutation(reference, normalizedArgs);
632
- },
633
- runAction: (reference, ...args) => this.runAction(reference, normalizeOptionalArgs(args))
634
- };
635
- }
636
- createDatabaseReader(state) {
637
- return {
638
- get: async (tableName, id) => {
639
- state.dependencyCollector?.add(`table:${tableName}`);
640
- state.dependencyCollector?.add(`row:${tableName}:${id}`);
641
- const row = await this.options.driver.get(`SELECT _id, _creationTime, _json FROM ${quoteIdentifier(tableName)} WHERE _id = ?`, [id]);
642
- return row ? this.deserializeDocument(tableName, row) : null;
643
- },
644
- query: (tableName) => new RuntimeQueryBuilder((options) => this.executeQueryBuilder(options), tableName, state.dependencyCollector),
645
- raw: (sql, params) => this.options.driver.all(sql, params)
646
- };
647
- }
648
- createDatabaseWriter(state) {
649
- const reader = this.createDatabaseReader(state);
650
- return {
651
- ...reader,
652
- insert: async (tableName, value) => {
653
- const validated = this.validateDocument(tableName, value);
654
- const id = generateId();
655
- const creationTime = Date.now();
656
- const json = stableStringify(validated);
657
- await this.options.driver.run(`INSERT INTO ${quoteIdentifier(tableName)} (_id, _creationTime, _json) VALUES (?, ?, ?)`, [
658
- id,
659
- creationTime,
660
- json
661
- ]);
662
- await this.syncSearchIndexes(tableName, {
663
- _id: id,
664
- _creationTime: creationTime,
665
- _json: json
666
- });
667
- state.changedTables.add(tableName);
668
- return id;
669
- },
670
- patch: async (tableName, id, value) => {
671
- const current = await reader.get(tableName, id);
672
- if (!current) throw new Error(`Document "${id}" does not exist in "${tableName}".`);
673
- const merged = {
674
- ...omitSystemFields(current),
675
- ...value
676
- };
677
- for (const key of Object.keys(merged)) if (merged[key] === void 0) delete merged[key];
678
- const validated = this.validateDocument(tableName, merged);
679
- await this.options.driver.run(`UPDATE ${quoteIdentifier(tableName)} SET _json = ? WHERE _id = ?`, [stableStringify(validated), id]);
680
- const row = await this.options.driver.get(`SELECT _id, _creationTime, _json FROM ${quoteIdentifier(tableName)} WHERE _id = ?`, [id]);
681
- if (row) await this.syncSearchIndexes(tableName, row);
682
- state.changedTables.add(tableName);
683
- },
684
- replace: async (tableName, id, value) => {
685
- const validated = this.validateDocument(tableName, value);
686
- await this.options.driver.run(`UPDATE ${quoteIdentifier(tableName)} SET _json = ? WHERE _id = ?`, [stableStringify(validated), id]);
687
- const row = await this.options.driver.get(`SELECT _id, _creationTime, _json FROM ${quoteIdentifier(tableName)} WHERE _id = ?`, [id]);
688
- if (!row) throw new Error(`Document "${id}" does not exist in "${tableName}".`);
689
- await this.syncSearchIndexes(tableName, row);
690
- state.changedTables.add(tableName);
691
- },
692
- delete: async (tableName, id) => {
693
- await this.options.driver.run(`DELETE FROM ${quoteIdentifier(tableName)} WHERE _id = ?`, [id]);
694
- await this.removeSearchIndexes(tableName, id);
695
- state.changedTables.add(tableName);
696
- }
697
- };
698
- }
699
- createStorageApi(state) {
700
- return {
701
- put: async (input) => {
702
- const id = generateId();
703
- const createdAt = Date.now();
704
- await this.options.driver.run(`INSERT OR REPLACE INTO "_storage_pending" (_id, _creationTime, file_name, content_type) VALUES (?, ?, ?, ?)`, [
705
- id,
706
- createdAt,
707
- input.fileName ?? null,
708
- input.contentType ?? null
709
- ]);
710
- const object = await this.options.storage.put(id, input);
711
- await this.options.driver.withTransaction(async () => {
712
- await this.options.driver.run(`INSERT OR REPLACE INTO "_storage" (_id, _creationTime, file_name, content_type, size, path) VALUES (?, ?, ?, ?, ?, ?)`, [
713
- id,
714
- createdAt,
715
- input.fileName ?? null,
716
- object.contentType,
717
- object.size,
718
- object.path
719
- ]);
720
- await this.options.driver.run(`DELETE FROM "_storage_pending" WHERE _id = ?`, [id]);
721
- });
722
- this.emitDevtools({
723
- type: "storage.updated",
724
- runtimeId: this.runtimeId,
725
- storageId: id,
726
- operation: "put",
727
- timestamp: Date.now()
728
- });
729
- state.storageChanges.push({
730
- storageId: id,
731
- reason: "storage-put"
732
- });
733
- return id;
734
- },
735
- get: async (id) => {
736
- const row = await this.options.driver.get(`SELECT _id, _creationTime, file_name, content_type, size, path FROM "_storage" WHERE _id = ?`, [id]);
737
- if (!row) return null;
738
- return {
739
- id: row._id,
740
- path: row.path,
741
- size: row.size,
742
- contentType: row.content_type
743
- };
744
- },
745
- read: async (id) => {
746
- if (!await this.options.driver.get(`SELECT _id FROM "_storage" WHERE _id = ?`, [id])) return null;
747
- return this.options.storage.read(id);
748
- },
749
- delete: async (id) => {
750
- await this.options.storage.delete(id);
751
- await this.options.driver.withTransaction(async () => {
752
- await this.options.driver.run(`DELETE FROM "_storage" WHERE _id = ?`, [id]);
753
- await this.options.driver.run(`DELETE FROM "_storage_pending" WHERE _id = ?`, [id]);
754
- });
755
- this.emitDevtools({
756
- type: "storage.updated",
757
- runtimeId: this.runtimeId,
758
- storageId: id,
759
- operation: "delete",
760
- timestamp: Date.now()
761
- });
762
- state.storageChanges.push({
763
- storageId: id,
764
- reason: "storage-delete"
765
- });
766
- }
767
- };
768
- }
769
- createSchedulerApi() {
770
- return {
771
- runAfter: async (delayMs, reference, ...args) => {
772
- const schedulerArgs = splitSchedulerArgs(args);
773
- const functionArgs = schedulerArgs[0];
774
- const misfirePolicy = schedulerArgs[1] ?? DEFAULT_MISFIRE_POLICY;
775
- return this.scheduleJob(Date.now() + delayMs, reference, functionArgs, misfirePolicy);
776
- },
777
- runAt: async (timestamp, reference, ...args) => {
778
- const schedulerArgs = splitSchedulerArgs(args);
779
- const functionArgs = schedulerArgs[0];
780
- const misfirePolicy = schedulerArgs[1] ?? DEFAULT_MISFIRE_POLICY;
781
- const value = timestamp instanceof Date ? timestamp.getTime() : timestamp;
782
- return this.scheduleJob(value, reference, functionArgs, misfirePolicy);
783
- },
784
- cancel: async (id) => {
785
- await this.cancelScheduledJob(id);
786
- }
787
- };
788
- }
789
- notifySchedulerJobsChanged() {
790
- this.notifyDevtoolsScopes(["scheduler.jobs"]);
791
- }
792
- async ensureSystemTables() {
793
- await this.options.driver.exec(`
794
- CREATE TABLE IF NOT EXISTS "_syncore_migrations" (
795
- id TEXT PRIMARY KEY,
796
- applied_at INTEGER NOT NULL,
797
- sql TEXT NOT NULL
798
- );
799
- CREATE TABLE IF NOT EXISTS "_syncore_schema_state" (
800
- id TEXT PRIMARY KEY,
801
- schema_hash TEXT NOT NULL,
802
- schema_json TEXT NOT NULL,
803
- updated_at INTEGER NOT NULL
804
- );
805
- CREATE TABLE IF NOT EXISTS "_storage" (
806
- _id TEXT PRIMARY KEY,
807
- _creationTime INTEGER NOT NULL,
808
- file_name TEXT,
809
- content_type TEXT,
810
- size INTEGER NOT NULL,
811
- path TEXT NOT NULL
812
- );
813
- CREATE TABLE IF NOT EXISTS "_storage_pending" (
814
- _id TEXT PRIMARY KEY,
815
- _creationTime INTEGER NOT NULL,
816
- file_name TEXT,
817
- content_type TEXT
818
- );
819
- CREATE TABLE IF NOT EXISTS "_scheduled_functions" (
820
- id TEXT PRIMARY KEY,
821
- function_name TEXT NOT NULL,
822
- function_kind TEXT NOT NULL,
823
- args_json TEXT NOT NULL,
824
- status TEXT NOT NULL,
825
- run_at INTEGER NOT NULL,
826
- created_at INTEGER NOT NULL,
827
- updated_at INTEGER NOT NULL,
828
- recurring_name TEXT,
829
- schedule_json TEXT,
830
- timezone TEXT,
831
- misfire_policy TEXT NOT NULL,
832
- last_run_at INTEGER,
833
- window_ms INTEGER
834
- );
835
- `);
836
- try {
837
- await this.options.driver.exec(`ALTER TABLE "_syncore_schema_state" ADD COLUMN schema_json TEXT NOT NULL DEFAULT '{}'`);
838
- } catch {}
839
- }
840
- async reconcileStorageState() {
841
- const pendingRows = await this.options.driver.all(`SELECT _id, _creationTime, file_name, content_type FROM "_storage_pending"`);
842
- for (const pendingRow of pendingRows) {
843
- if (!await this.options.driver.get(`SELECT _id FROM "_storage" WHERE _id = ?`, [pendingRow._id])) {
844
- await this.options.storage.delete(pendingRow._id);
845
- this.emitDevtools({
846
- type: "log",
847
- runtimeId: this.runtimeId,
848
- level: "warn",
849
- message: `Recovered interrupted storage write ${pendingRow._id}.`,
850
- timestamp: Date.now()
851
- });
852
- }
853
- await this.options.driver.run(`DELETE FROM "_storage_pending" WHERE _id = ?`, [pendingRow._id]);
854
- }
855
- if (!this.options.storage.list) return;
856
- const storedRows = await this.options.driver.all(`SELECT _id FROM "_storage"`);
857
- const knownIds = new Set(storedRows.map((row) => row._id));
858
- const physicalObjects = await this.options.storage.list();
859
- for (const object of physicalObjects) {
860
- if (knownIds.has(object.id)) continue;
861
- await this.options.storage.delete(object.id);
862
- this.emitDevtools({
863
- type: "log",
864
- runtimeId: this.runtimeId,
865
- level: "warn",
866
- message: `Removed orphaned storage object ${object.id}.`,
867
- timestamp: Date.now()
868
- });
869
- }
870
- }
871
- async applySchema() {
872
- const nextSnapshot = createSchemaSnapshot(this.options.schema);
873
- const stateRow = await this.options.driver.get(`SELECT schema_hash, schema_json FROM "_syncore_schema_state" WHERE id = 'current'`);
874
- let previousSnapshot = null;
875
- if (stateRow?.schema_json && stateRow.schema_json !== "{}") try {
876
- previousSnapshot = parseSchemaSnapshot(stateRow.schema_json);
877
- } catch {
878
- previousSnapshot = null;
879
- }
880
- const plan = diffSchemaSnapshots(previousSnapshot, nextSnapshot);
881
- if (plan.destructiveChanges.length > 0) throw new Error(`Syncore detected destructive schema changes that require a manual migration:\n${plan.destructiveChanges.join("\n")}`);
882
- for (const warning of plan.warnings) this.emitDevtools({
883
- type: "log",
884
- runtimeId: this.runtimeId,
885
- level: "warn",
886
- message: warning,
887
- timestamp: Date.now()
888
- });
889
- for (const statement of plan.statements) {
890
- const searchKey = this.findSearchIndexKeyForStatement(statement);
891
- try {
892
- await this.options.driver.exec(statement);
893
- } catch (error) {
894
- if (searchKey) {
895
- this.disabledSearchIndexes.add(searchKey);
896
- this.emitDevtools({
897
- type: "log",
898
- runtimeId: this.runtimeId,
899
- level: "warn",
900
- message: `FTS5 unavailable for ${searchKey}; falling back to LIKE search.`,
901
- timestamp: Date.now()
902
- });
903
- continue;
904
- }
905
- throw error;
906
- }
907
- }
908
- if (plan.statements.length > 0 || plan.warnings.length > 0) {
909
- const migrationSql = renderMigrationSql(plan, { title: "Syncore automatic schema reconciliation" });
910
- await this.options.driver.run(`INSERT OR REPLACE INTO "_syncore_migrations" (id, applied_at, sql) VALUES (?, ?, ?)`, [
911
- nextSnapshot.hash,
912
- Date.now(),
913
- migrationSql
914
- ]);
915
- }
916
- await this.options.driver.run(`INSERT INTO "_syncore_schema_state" (id, schema_hash, schema_json, updated_at)
917
- VALUES ('current', ?, ?, ?)
918
- ON CONFLICT(id) DO UPDATE SET schema_hash = excluded.schema_hash, schema_json = excluded.schema_json, updated_at = excluded.updated_at`, [
919
- nextSnapshot.hash,
920
- stableStringify(nextSnapshot),
921
- Date.now()
922
- ]);
923
- for (const tableName of this.options.schema.tableNames()) {
924
- const table = this.getTableDefinition(tableName);
925
- for (const searchIndex of table.searchIndexes) {
926
- const searchKey = `${tableName}:${searchIndex.name}`;
927
- try {
928
- await this.options.driver.exec(renderCreateSearchIndexStatement(tableName, searchIndex));
929
- this.disabledSearchIndexes.delete(searchKey);
930
- } catch {
931
- const alreadyDisabled = this.disabledSearchIndexes.has(searchKey);
932
- this.disabledSearchIndexes.add(searchKey);
933
- if (!alreadyDisabled) this.emitDevtools({
934
- type: "log",
935
- runtimeId: this.runtimeId,
936
- level: "warn",
937
- message: `FTS5 unavailable for ${searchKey}; falling back to LIKE search.`,
938
- timestamp: Date.now()
939
- });
940
- }
941
- }
942
- }
943
- }
944
- async scheduleJob(runAt, reference, args, misfirePolicy) {
945
- const id = generateId();
946
- const now = Date.now();
947
- await this.options.driver.run(`INSERT INTO "_scheduled_functions"
948
- (id, function_name, function_kind, args_json, status, run_at, created_at, updated_at, recurring_name, schedule_json, timezone, misfire_policy, last_run_at, window_ms)
949
- VALUES (?, ?, ?, ?, 'scheduled', ?, ?, ?, NULL, NULL, NULL, ?, NULL, ?)`, [
950
- id,
951
- reference.name,
952
- reference.kind,
953
- stableStringify(args),
954
- runAt,
955
- now,
956
- now,
957
- misfirePolicy.type,
958
- misfirePolicy.type === "windowed" ? misfirePolicy.windowMs : null
959
- ]);
960
- this.notifySchedulerJobsChanged();
961
- return id;
962
- }
963
- async syncRecurringJobs() {
964
- for (const job of this.recurringJobs) {
965
- const id = `recurring:${job.name}`;
966
- if (await this.options.driver.get(`SELECT * FROM "_scheduled_functions" WHERE id = ?`, [id])) continue;
967
- const nextRunAt = computeNextRun(job.schedule, Date.now());
968
- await this.options.driver.run(`INSERT INTO "_scheduled_functions"
969
- (id, function_name, function_kind, args_json, status, run_at, created_at, updated_at, recurring_name, schedule_json, timezone, misfire_policy, last_run_at, window_ms)
970
- VALUES (?, ?, ?, ?, 'scheduled', ?, ?, ?, ?, ?, ?, ?, NULL, ?)`, [
971
- id,
972
- job.function.name,
973
- job.function.kind,
974
- stableStringify(job.args),
975
- nextRunAt,
976
- Date.now(),
977
- Date.now(),
978
- job.name,
979
- stableStringify(job.schedule),
980
- "timezone" in job.schedule ? job.schedule.timezone ?? null : null,
981
- job.misfirePolicy.type,
982
- job.misfirePolicy.type === "windowed" ? job.misfirePolicy.windowMs : null
983
- ]);
984
- this.notifySchedulerJobsChanged();
985
- }
986
- }
987
- async processDueJobs() {
988
- const now = Date.now();
989
- const dueJobs = await this.options.driver.all(`SELECT * FROM "_scheduled_functions" WHERE status = 'scheduled' AND run_at <= ? ORDER BY run_at ASC`, [now]);
990
- const executedJobIds = [];
991
- for (const job of dueJobs) {
992
- const misfirePolicy = parseMisfirePolicy(job.misfire_policy, job.window_ms);
993
- if (!shouldRunMissedJob(job.run_at, now, misfirePolicy)) {
994
- await this.advanceOrFinalizeJob(job, "skipped", now);
995
- continue;
996
- }
997
- try {
998
- if (job.function_kind === "mutation") await this.runMutation({
999
- kind: "mutation",
1000
- name: job.function_name
1001
- }, JSON.parse(job.args_json));
1002
- else await this.runAction({
1003
- kind: "action",
1004
- name: job.function_name
1005
- }, JSON.parse(job.args_json));
1006
- executedJobIds.push(job.id);
1007
- await this.advanceOrFinalizeJob(job, "completed", now);
1008
- } catch (error) {
1009
- await this.options.driver.run(`UPDATE "_scheduled_functions" SET status = 'failed', updated_at = ? WHERE id = ?`, [Date.now(), job.id]);
1010
- this.notifySchedulerJobsChanged();
1011
- this.emitDevtools({
1012
- type: "log",
1013
- runtimeId: this.runtimeId,
1014
- level: "error",
1015
- message: `Scheduled job ${job.id} failed: ${error instanceof Error ? error.message : String(error)}`,
1016
- timestamp: Date.now()
1017
- });
1018
- }
1019
- }
1020
- if (executedJobIds.length > 0) {
1021
- this.emitDevtools({
1022
- type: "scheduler.tick",
1023
- runtimeId: this.runtimeId,
1024
- executedJobIds,
1025
- timestamp: Date.now()
1026
- });
1027
- this.notifySchedulerJobsChanged();
1028
- }
1029
- }
1030
- async advanceOrFinalizeJob(job, terminalStatus, executedAt) {
1031
- if (!job.recurring_name || !job.schedule_json) {
1032
- await this.options.driver.run(`UPDATE "_scheduled_functions" SET status = ?, updated_at = ?, last_run_at = ? WHERE id = ?`, [
1033
- terminalStatus,
1034
- executedAt,
1035
- executedAt,
1036
- job.id
1037
- ]);
1038
- this.notifySchedulerJobsChanged();
1039
- return;
1040
- }
1041
- const nextRunAt = computeNextRun(JSON.parse(job.schedule_json), executedAt + 1);
1042
- await this.options.driver.run(`UPDATE "_scheduled_functions"
1043
- SET status = 'scheduled', run_at = ?, updated_at = ?, last_run_at = ?
1044
- WHERE id = ?`, [
1045
- nextRunAt,
1046
- executedAt,
1047
- executedAt,
1048
- job.id
1049
- ]);
1050
- this.notifySchedulerJobsChanged();
1051
- }
1052
- async refreshInvalidatedQueries(changedTables, mutationId) {
1053
- for (const query of this.activeQueries.values()) {
1054
- if (![...changedTables].some((tableName) => query.dependencyKeys.has(`table:${tableName}`))) continue;
1055
- this.emitDevtools({
1056
- type: "query.invalidated",
1057
- runtimeId: this.runtimeId,
1058
- queryId: query.id,
1059
- reason: `Mutation ${mutationId} changed ${[...changedTables].join(", ")}`,
1060
- timestamp: Date.now()
1061
- });
1062
- await this.rerunActiveQuery(query);
1063
- }
1064
- }
1065
- async rerunActiveQuery(record) {
1066
- record.dependencyKeys.clear();
1067
- try {
1068
- record.lastResult = await this.runQuery({
1069
- kind: "query",
1070
- name: record.functionName
1071
- }, record.args);
1072
- record.lastError = void 0;
1073
- record.lastRunAt = Date.now();
1074
- record.dependencyKeys = await this.collectQueryDependencies(record.functionName, record.args);
1075
- } catch (error) {
1076
- record.lastError = error;
1077
- }
1078
- for (const listener of record.listeners) listener();
1079
- }
1080
- async handleExternalChangeEvent(event) {
1081
- if (event.sourceId === this.externalChangeSourceId) return;
1082
- const result = this.options.externalChangeApplier ? await this.options.externalChangeApplier.applyExternalChange(event) : {
1083
- databaseChanged: event.scope === "database" || event.scope === "all",
1084
- storageChanged: event.scope === "storage" || event.scope === "all"
1085
- };
1086
- await this.processExternalChangeResult(result);
1087
- }
1088
- async processExternalChangeResult(result) {
1089
- if (!result.databaseChanged && !result.storageChanged) return;
1090
- if (this.pendingExternalChangePromise) {
1091
- this.queuedExternalChange = {
1092
- databaseChanged: (this.queuedExternalChange?.databaseChanged ?? false) || result.databaseChanged,
1093
- storageChanged: (this.queuedExternalChange?.storageChanged ?? false) || result.storageChanged
1094
- };
1095
- return this.pendingExternalChangePromise;
1096
- }
1097
- this.pendingExternalChangePromise = (async () => {
1098
- if (result.databaseChanged || result.storageChanged) await this.refreshAllActiveQueries();
1099
- })();
1100
- try {
1101
- await this.pendingExternalChangePromise;
1102
- } finally {
1103
- this.pendingExternalChangePromise = void 0;
1104
- const queued = this.queuedExternalChange;
1105
- this.queuedExternalChange = void 0;
1106
- if (queued) await this.processExternalChangeResult(queued);
1107
- }
1108
- }
1109
- async publishExternalChange(event) {
1110
- await this.options.externalChangeSignal?.publish({
1111
- ...event,
1112
- sourceId: this.externalChangeSourceId,
1113
- timestamp: Date.now()
1114
- });
1115
- }
1116
- async publishStorageChanges(storageChanges) {
1117
- for (const change of storageChanges) await this.publishExternalChange({
1118
- scope: "storage",
1119
- reason: change.reason,
1120
- storageIds: [change.storageId]
1121
- });
1122
- }
1123
- async refreshAllActiveQueries() {
1124
- for (const query of this.activeQueries.values()) await this.rerunActiveQuery(query);
1125
- }
1126
- async getSchemaTablesForDevtools() {
1127
- const tables = [];
1128
- for (const name of this.options.schema.tableNames()) {
1129
- const table = this.getTableDefinition(name);
1130
- const validatorDesc = describeValidator(table.validator);
1131
- const fields = validatorDesc.kind === "object" ? Object.entries(validatorDesc.shape).map(([fieldName, fieldDesc]) => {
1132
- const desc = fieldDesc;
1133
- const optional = desc.kind === "optional";
1134
- return {
1135
- name: fieldName,
1136
- type: optional ? desc.inner?.kind ?? "any" : desc.kind,
1137
- optional
1138
- };
1139
- }) : [];
1140
- fields.unshift({
1141
- name: "_id",
1142
- type: "string",
1143
- optional: false
1144
- }, {
1145
- name: "_creationTime",
1146
- type: "number",
1147
- optional: false
1148
- });
1149
- let documentCount = 0;
1150
- try {
1151
- documentCount = (await this.options.driver.get(`SELECT COUNT(*) as count FROM ${quoteIdentifier(name)}`))?.count ?? 0;
1152
- } catch {
1153
- documentCount = 0;
1154
- }
1155
- tables.push({
1156
- name,
1157
- fields,
1158
- indexes: table.indexes.map((index) => ({
1159
- name: index.name,
1160
- fields: index.fields,
1161
- unique: false
1162
- })),
1163
- documentCount
1164
- });
1165
- }
1166
- return tables;
1167
- }
1168
- async collectQueryDependencies(functionName, args) {
1169
- const definition = this.resolveFunction({
1170
- kind: "query",
1171
- name: functionName
1172
- }, "query");
1173
- const dependencyCollector = /* @__PURE__ */ new Set();
1174
- await this.invokeFunction(definition, args, {
1175
- mutationDepth: 0,
1176
- changedTables: /* @__PURE__ */ new Set(),
1177
- storageChanges: [],
1178
- dependencyCollector
1179
- });
1180
- return dependencyCollector;
1181
- }
1182
- resolveFunction(reference, expectedKind) {
1183
- const definition = this.options.functions[reference.name];
1184
- if (!definition) throw new Error(`Unknown function "${reference.name}".`);
1185
- if (definition.kind !== expectedKind) throw new Error(`Function "${reference.name}" is a ${definition.kind}, expected ${expectedKind}.`);
1186
- return definition;
1187
- }
1188
- validateDocument(tableName, value) {
1189
- return this.getTableDefinition(tableName).validator.parse(value);
1190
- }
1191
- deserializeDocument(tableName, row) {
1192
- const payload = JSON.parse(row._json);
1193
- const document = {
1194
- ...payload,
1195
- _id: row._id,
1196
- _creationTime: row._creationTime
1197
- };
1198
- this.getTableDefinition(tableName).validator.parse(payload);
1199
- return document;
1200
- }
1201
- async syncSearchIndexes(tableName, row) {
1202
- const table = this.getTableDefinition(tableName);
1203
- if (table.searchIndexes.length === 0) return;
1204
- const payload = JSON.parse(row._json);
1205
- for (const searchIndex of table.searchIndexes) {
1206
- if (this.disabledSearchIndexes.has(`${tableName}:${searchIndex.name}`)) continue;
1207
- await this.options.driver.run(`DELETE FROM ${quoteIdentifier(searchIndexTableName(tableName, searchIndex.name))} WHERE _id = ?`, [row._id]);
1208
- await this.options.driver.run(`INSERT INTO ${quoteIdentifier(searchIndexTableName(tableName, searchIndex.name))} (_id, search_value) VALUES (?, ?)`, [row._id, toSearchValue(payload[searchIndex.searchField])]);
1209
- }
1210
- }
1211
- async removeSearchIndexes(tableName, id) {
1212
- const table = this.getTableDefinition(tableName);
1213
- for (const searchIndex of table.searchIndexes) {
1214
- if (this.disabledSearchIndexes.has(`${tableName}:${searchIndex.name}`)) continue;
1215
- await this.options.driver.run(`DELETE FROM ${quoteIdentifier(searchIndexTableName(tableName, searchIndex.name))} WHERE _id = ?`, [id]);
1216
- }
1217
- }
1218
- renderExpression(tableAlias, expression, params) {
1219
- if (expression.type === "condition") return this.renderCondition(tableAlias, expression.condition, params);
1220
- const separator = expression.type === "and" ? " AND " : " OR ";
1221
- return `(${expression.expressions.map((child) => this.renderExpression(tableAlias, child, params)).join(separator)})`;
1222
- }
1223
- renderCondition(tableAlias, condition, params) {
1224
- params.push(condition.value);
1225
- return `${fieldExpression(tableAlias, condition.field)} ${condition.operator} ?`;
1226
- }
1227
- createActiveQueryKey(name, args) {
1228
- return `${name}:${stableStringify(args)}`;
1229
- }
1230
- emitDevtools(event) {
1231
- this.recentEvents.unshift(event);
1232
- this.recentEvents.splice(24);
1233
- this.options.devtools?.emit(event);
1234
- this.notifyDevtoolsScopes(devtoolsScopesForEvent(event));
1235
- for (const listener of this.devtoolsListeners) listener(event);
1236
- }
1237
- createPluginContext() {
1238
- return {
1239
- runtimeId: this.runtimeId,
1240
- platform: this.platform,
1241
- schema: this.options.schema,
1242
- driver: this.options.driver,
1243
- storage: this.options.storage,
1244
- ...this.options.scheduler ? { scheduler: this.options.scheduler } : {},
1245
- ...this.options.devtools ? { devtools: this.options.devtools } : {},
1246
- emitDevtools: (event) => {
1247
- this.emitDevtools(event);
1248
- }
1249
- };
1250
- }
1251
- buildCapabilities() {
1252
- const capabilities = { ...this.options.capabilities ?? {} };
1253
- for (const plugin of this.experimentalPlugins) {
1254
- if (!plugin.capabilities) continue;
1255
- const contributed = typeof plugin.capabilities === "function" ? plugin.capabilities(this.createPluginContext()) : plugin.capabilities;
1256
- if (!contributed) continue;
1257
- Object.assign(capabilities, contributed);
1258
- }
1259
- return capabilities;
1260
- }
1261
- async runPluginHook(hook) {
1262
- const context = this.createPluginContext();
1263
- for (const plugin of this.experimentalPlugins) {
1264
- const handler = plugin[hook];
1265
- if (!handler) continue;
1266
- await handler(context);
1267
- }
1268
- }
1269
- findSearchIndexKeyForStatement(statement) {
1270
- for (const tableName of this.options.schema.tableNames()) {
1271
- const table = this.getTableDefinition(tableName);
1272
- for (const searchIndex of table.searchIndexes) if (statement === renderCreateSearchIndexStatement(tableName, searchIndex)) return `${tableName}:${searchIndex.name}`;
1273
- }
1274
- return null;
1275
- }
1276
- getTableDefinition(tableName) {
1277
- return this.options.schema.getTable(tableName);
37
+ return this.kernel.watchQuery(reference, args);
1278
38
  }
1279
39
  };
1280
- function fieldExpression(tableAlias, field) {
1281
- return `json_extract(${tableAlias ? `${tableAlias}.` : ""}_json, '$.${field}')`;
1282
- }
1283
- function quoteIdentifier(identifier) {
1284
- return `"${identifier.replaceAll("\"", "\"\"")}"`;
1285
- }
1286
- function stableStringify(value) {
1287
- return JSON.stringify(sortValue(value));
1288
- }
1289
- function sortValue(value) {
1290
- if (Array.isArray(value)) return value.map(sortValue);
1291
- if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, nested]) => [key, sortValue(nested)]));
1292
- return value;
1293
- }
1294
- function devtoolsScopesForEvent(event) {
1295
- switch (event.type) {
1296
- case "runtime.connected":
1297
- case "runtime.disconnected": return new Set(["runtime.summary", "runtime.activeQueries"]);
1298
- case "query.executed":
1299
- case "query.invalidated": return new Set(["runtime.summary", "runtime.activeQueries"]);
1300
- case "mutation.committed": return new Set(["runtime.summary", ...event.changedTables.map((table) => `table:${table}`)]);
1301
- case "scheduler.tick": return new Set(["scheduler.jobs", "runtime.summary"]);
1302
- case "storage.updated": return new Set(["runtime.summary"]);
1303
- case "action.completed":
1304
- case "log": return new Set(["runtime.summary"]);
1305
- }
1306
- }
1307
- function inferDriverDatabasePath(driver) {
1308
- const candidate = driver;
1309
- return candidate.databasePath ?? candidate.filename;
1310
- }
1311
- function omitSystemFields(document) {
1312
- const clone = { ...document };
1313
- delete clone._id;
1314
- delete clone._creationTime;
1315
- return clone;
1316
- }
1317
- function toSearchValue(value) {
1318
- if (typeof value === "string") return value;
1319
- if (value === null || value === void 0) return "";
1320
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
1321
- return stableStringify(value);
1322
- }
1323
- function parseMisfirePolicy(type, windowMs) {
1324
- if (type === "windowed") return {
1325
- type,
1326
- windowMs: windowMs ?? 0
1327
- };
1328
- if (type === "skip" || type === "run_once_if_missed") return { type };
1329
- return { type: "catch_up" };
1330
- }
1331
- function shouldRunMissedJob(scheduledAt, now, policy) {
1332
- if (scheduledAt >= now) return true;
1333
- switch (policy.type) {
1334
- case "catch_up": return true;
1335
- case "run_once_if_missed": return true;
1336
- case "skip": return false;
1337
- case "windowed": return now - scheduledAt <= policy.windowMs;
1338
- }
1339
- }
1340
- function computeNextRun(schedule, fromTimestamp) {
1341
- switch (schedule.type) {
1342
- case "interval": return fromTimestamp + intervalToMs(schedule);
1343
- case "daily": return nextDailyOccurrence(fromTimestamp, schedule);
1344
- case "weekly": return nextWeeklyOccurrence(fromTimestamp, schedule);
1345
- }
1346
- }
1347
- function intervalToMs(schedule) {
1348
- if (schedule.seconds) return schedule.seconds * 1e3;
1349
- if (schedule.minutes) return schedule.minutes * 60 * 1e3;
1350
- return (schedule.hours ?? 1) * 60 * 60 * 1e3;
1351
- }
1352
- function nextDailyOccurrence(fromTimestamp, schedule) {
1353
- const timezone = schedule.timezone ?? "UTC";
1354
- const zonedNow = toZonedTime(new Date(fromTimestamp), timezone);
1355
- const zoned = new Date(zonedNow.getTime());
1356
- zoned.setHours(schedule.hour, schedule.minute, 0, 0);
1357
- if (zoned.getTime() <= zonedNow.getTime()) zoned.setDate(zoned.getDate() + 1);
1358
- return fromZonedTime(zoned, timezone).getTime();
1359
- }
1360
- function nextWeeklyOccurrence(fromTimestamp, schedule) {
1361
- const timezone = schedule.timezone ?? "UTC";
1362
- const zonedNow = toZonedTime(new Date(fromTimestamp), timezone);
1363
- const targetDay = [
1364
- "sunday",
1365
- "monday",
1366
- "tuesday",
1367
- "wednesday",
1368
- "thursday",
1369
- "friday",
1370
- "saturday"
1371
- ].indexOf(schedule.dayOfWeek);
1372
- const zoned = new Date(zonedNow.getTime());
1373
- const delta = (targetDay - zonedNow.getDay() + 7) % 7;
1374
- zoned.setDate(zoned.getDate() + delta);
1375
- zoned.setHours(schedule.hour, schedule.minute, 0, 0);
1376
- if (zoned.getTime() <= zonedNow.getTime()) zoned.setDate(zoned.getDate() + 7);
1377
- return fromZonedTime(zoned, timezone).getTime();
1378
- }
1379
40
  function createFunctionReference(kind, name) {
1380
41
  return {
1381
42
  kind,
@@ -1383,9 +44,8 @@ function createFunctionReference(kind, name) {
1383
44
  };
1384
45
  }
1385
46
  /**
1386
- * Create a typed function reference from a concrete Syncore function definition.
1387
- *
1388
- * Generated code uses this helper to preserve function arg and result inference.
47
+ * Create a function reference from an existing Syncore function definition
48
+ * while preserving its inferred args and result types.
1389
49
  */
1390
50
  function createFunctionReferenceFor(kind, name) {
1391
51
  return {
@@ -1393,21 +53,6 @@ function createFunctionReferenceFor(kind, name) {
1393
53
  name
1394
54
  };
1395
55
  }
1396
- function normalizeOptionalArgs(args) {
1397
- return args[0] ?? {};
1398
- }
1399
- function splitSchedulerArgs(args) {
1400
- if (args.length === 0) return [{}, void 0];
1401
- if (args.length === 1) {
1402
- const [first] = args;
1403
- if (isMisfirePolicy(first)) return [{}, first];
1404
- return [first ?? {}, void 0];
1405
- }
1406
- return [args[0] ?? {}, args[1]];
1407
- }
1408
- function isMisfirePolicy(value) {
1409
- return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
1410
- }
1411
56
  //#endregion
1412
57
  export { SyncoreRuntime, createFunctionReference, createFunctionReferenceFor };
1413
58