@edge-base/server 0.2.2 → 0.2.4

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 (97) hide show
  1. package/admin-build/_app/immutable/chunks/{C85dMlzL.js → 5RQRbp5q.js} +1 -1
  2. package/admin-build/_app/immutable/chunks/{B8DT4fss.js → BME_U9TJ.js} +1 -1
  3. package/admin-build/_app/immutable/chunks/{BaCHY17I.js → BYI6CUvd.js} +1 -1
  4. package/admin-build/_app/immutable/chunks/{BWyDPAjM.js → BgDzp0i0.js} +1 -1
  5. package/admin-build/_app/immutable/chunks/{c5iKSdWY.js → BjWZuf8W.js} +1 -1
  6. package/admin-build/_app/immutable/chunks/{g3ZZdY-r.js → C6lpZLE2.js} +1 -1
  7. package/admin-build/_app/immutable/chunks/{C-DsDCNG.js → D5GswVnI.js} +3 -3
  8. package/admin-build/_app/immutable/chunks/DBsVqhuh.js +1 -0
  9. package/admin-build/_app/immutable/chunks/{BEYYl662.js → DYaCRWMA.js} +1 -1
  10. package/admin-build/_app/immutable/chunks/D__dwMuW.js +1 -0
  11. package/admin-build/_app/immutable/chunks/{4vlsb8ej.js → Dj-E9-FO.js} +1 -1
  12. package/admin-build/_app/immutable/chunks/{kiJ6KthZ.js → Dj0QUuOf.js} +1 -1
  13. package/admin-build/_app/immutable/chunks/{BKXmgPq4.js → XQM1k9PM.js} +1 -1
  14. package/admin-build/_app/immutable/chunks/{CTngeX8H.js → fYEKMQ-Z.js} +1 -1
  15. package/admin-build/_app/immutable/chunks/{CPdXvRUb.js → g_-Kpxu3.js} +1 -1
  16. package/admin-build/_app/immutable/chunks/{DzXaj-Ja.js → wCNueVYy.js} +1 -1
  17. package/admin-build/_app/immutable/entry/{app.BZxfavhF.js → app.C8ylfBe6.js} +2 -2
  18. package/admin-build/_app/immutable/entry/start.CtsqDyfj.js +1 -0
  19. package/admin-build/_app/immutable/nodes/{0.DlsaydXO.js → 0.CJJ6HZbp.js} +1 -1
  20. package/admin-build/_app/immutable/nodes/{1.D2NWN5eG.js → 1.B4sI5cB4.js} +1 -1
  21. package/admin-build/_app/immutable/nodes/{10.EMDaN3nw.js → 10.D6hvCer6.js} +1 -1
  22. package/admin-build/_app/immutable/nodes/{11.BasqQ_o9.js → 11.Dx7b8aQ5.js} +1 -1
  23. package/admin-build/_app/immutable/nodes/{12.DO31Ljs7.js → 12.Bqmy5KIF.js} +1 -1
  24. package/admin-build/_app/immutable/nodes/{13.DhyAy-GZ.js → 13.CC6KpXgS.js} +1 -1
  25. package/admin-build/_app/immutable/nodes/{14.CLecGWc4.js → 14.yCo1Ix8E.js} +1 -1
  26. package/admin-build/_app/immutable/nodes/{15.B9kp3W4e.js → 15.co0UfPlh.js} +1 -1
  27. package/admin-build/_app/immutable/nodes/{16.Pu_8T3RI.js → 16.D0xkPUBW.js} +1 -1
  28. package/admin-build/_app/immutable/nodes/{17.DX4z43t6.js → 17.CebNqPeh.js} +1 -1
  29. package/admin-build/_app/immutable/nodes/{18.BKsSaxrr.js → 18.JUoLOZxh.js} +1 -1
  30. package/admin-build/_app/immutable/nodes/{19.DXNF1htN.js → 19.ND8kmQJe.js} +1 -1
  31. package/admin-build/_app/immutable/nodes/{20.VRVb0wee.js → 20.DYb-q3W8.js} +1 -1
  32. package/admin-build/_app/immutable/nodes/21.cz3IN9Cc.js +1 -0
  33. package/admin-build/_app/immutable/nodes/{22.DqZf4CtH.js → 22.UOzm8WYV.js} +1 -1
  34. package/admin-build/_app/immutable/nodes/{23.DtyxMiQG.js → 23.BLgq21om.js} +1 -1
  35. package/admin-build/_app/immutable/nodes/{24.CloWNmTd.js → 24.DN9usmUs.js} +1 -1
  36. package/admin-build/_app/immutable/nodes/{25.CnZWMq7_.js → 25.BddRfAyE.js} +1 -1
  37. package/admin-build/_app/immutable/nodes/{26.DrV7XOmf.js → 26.Dl6XHIeT.js} +1 -1
  38. package/admin-build/_app/immutable/nodes/{27.DV8L32OF.js → 27.D0iNwALG.js} +1 -1
  39. package/admin-build/_app/immutable/nodes/{28.Stil2D4u.js → 28.9dKQmdGi.js} +1 -1
  40. package/admin-build/_app/immutable/nodes/{29.Zsm1e5Dc.js → 29.wXzfJUXp.js} +1 -1
  41. package/admin-build/_app/immutable/nodes/{3.CKoj2vNz.js → 3.z8ut3jS-.js} +1 -1
  42. package/admin-build/_app/immutable/nodes/{30.Ni0k5bER.js → 30.BtZETNsL.js} +1 -1
  43. package/admin-build/_app/immutable/nodes/{31.mnqj9EbV.js → 31.CYonj2Jh.js} +1 -1
  44. package/admin-build/_app/immutable/nodes/{4.B_-z9AzT.js → 4.COtDPQ9b.js} +1 -1
  45. package/admin-build/_app/immutable/nodes/{5.yiZ72j4k.js → 5.CTRCeIhp.js} +1 -1
  46. package/admin-build/_app/immutable/nodes/{6.BqykybBG.js → 6.ChHi3QkR.js} +1 -1
  47. package/admin-build/_app/immutable/nodes/{7.BDAHlhsF.js → 7.CCMtr6Ac.js} +1 -1
  48. package/admin-build/_app/immutable/nodes/{8.D8Xvy0lH.js → 8.DpWJ-X_-.js} +1 -1
  49. package/admin-build/_app/immutable/nodes/{9.Dddmd7_F.js → 9.DOkvfmir.js} +1 -1
  50. package/admin-build/_app/version.json +1 -1
  51. package/admin-build/index.html +7 -7
  52. package/package.json +3 -3
  53. package/src/__tests__/admin-data-routes.test.ts +29 -0
  54. package/src/__tests__/d1-live-broadcast-verification.test.ts +271 -0
  55. package/src/__tests__/database-do-route-validation.test.ts +105 -0
  56. package/src/__tests__/database-live-do.test.ts +50 -0
  57. package/src/__tests__/database-live-emitter.test.ts +116 -1
  58. package/src/__tests__/database-live-route.test.ts +82 -0
  59. package/src/__tests__/do-router.test.ts +116 -0
  60. package/src/__tests__/error-format.test.ts +63 -0
  61. package/src/__tests__/functions-context.test.ts +674 -33
  62. package/src/__tests__/functions-d1-proxy.test.ts +54 -0
  63. package/src/__tests__/plugin-migration-routing.test.ts +32 -0
  64. package/src/__tests__/postgres-field-ops-compat.test.ts +110 -0
  65. package/src/__tests__/provider-aware-sql.test.ts +163 -0
  66. package/src/__tests__/room-auth-state-loss.test.ts +124 -0
  67. package/src/__tests__/runtime-surface-accounting.test.ts +0 -4
  68. package/src/__tests__/scheduled.test.ts +55 -0
  69. package/src/__tests__/service-key-db-proxy.test.ts +122 -1
  70. package/src/__tests__/sql-route.test.ts +252 -75
  71. package/src/__tests__/table-hook-runtime.test.ts +137 -0
  72. package/src/durable-objects/database-do.ts +36 -45
  73. package/src/durable-objects/database-live-do.ts +46 -1
  74. package/src/durable-objects/room-runtime-base.ts +26 -2
  75. package/src/durable-objects/rooms-do.ts +1 -1
  76. package/src/index.ts +12 -6
  77. package/src/lib/admin-db-target.ts +30 -74
  78. package/src/lib/d1-handler.ts +55 -35
  79. package/src/lib/database-live-emitter.ts +57 -16
  80. package/src/lib/do-router.ts +135 -3
  81. package/src/lib/functions.ts +215 -143
  82. package/src/lib/internal-transport.ts +28 -12
  83. package/src/lib/plugin-migration-routing.ts +28 -0
  84. package/src/lib/plugin-migrations.ts +38 -38
  85. package/src/lib/postgres-handler.ts +51 -31
  86. package/src/lib/provider-aware-sql.ts +831 -0
  87. package/src/lib/table-hook-runtime.ts +62 -0
  88. package/src/routes/admin.ts +41 -41
  89. package/src/routes/auth.ts +7 -2
  90. package/src/routes/database-live.ts +110 -12
  91. package/src/routes/sql.ts +64 -84
  92. package/src/routes/storage.ts +7 -2
  93. package/src/routes/tables.ts +42 -29
  94. package/admin-build/_app/immutable/chunks/5PDcRlfX.js +0 -1
  95. package/admin-build/_app/immutable/chunks/qiZXAKh-.js +0 -1
  96. package/admin-build/_app/immutable/entry/start.Mr9mmopc.js +0 -1
  97. package/admin-build/_app/immutable/nodes/21.Ck3_0D2f.js +0 -1
@@ -0,0 +1,28 @@
1
+ const PLUGIN_MIGRATION_PATH_PREFIXES = [
2
+ '/api/auth',
3
+ '/api/db',
4
+ '/api/functions',
5
+ '/api/sql',
6
+ '/api/storage',
7
+ '/admin/api',
8
+ ] as const;
9
+
10
+ export function shouldRunPluginMigrationsForRequestPath(path: string): boolean {
11
+ if (!path.startsWith('/')) {
12
+ path = `/${path}`;
13
+ }
14
+
15
+ if (
16
+ path === '/admin/api/backup'
17
+ || path.startsWith('/admin/api/backup/')
18
+ || path.startsWith('/admin/api/data/backup/')
19
+ || path === '/internal/backup'
20
+ || path.startsWith('/internal/backup/')
21
+ ) {
22
+ return false;
23
+ }
24
+
25
+ return PLUGIN_MIGRATION_PATH_PREFIXES.some(
26
+ (prefix) => path === prefix || path.startsWith(`${prefix}/`),
27
+ );
28
+ }
@@ -33,7 +33,7 @@ import {
33
33
  buildFunctionPushProxy,
34
34
  buildAdminAuthContext,
35
35
  } from './functions.js';
36
- import { executeDoSql } from './do-sql.js';
36
+ import { executeProviderAwareSql } from './provider-aware-sql.js';
37
37
  import { resolveRootServiceKey } from './service-key.js';
38
38
 
39
39
  /**
@@ -118,9 +118,9 @@ function arePluginMigrationsCurrentInMemory(plugins: PluginInstance[]): boolean
118
118
  return false;
119
119
  }
120
120
 
121
- return versionedPlugins.every((plugin) => (
122
- currentVersionedPlugins.get(plugin.name) === plugin.version
123
- ));
121
+ return versionedPlugins.every(
122
+ (plugin) => currentVersionedPlugins.get(plugin.name) === plugin.version,
123
+ );
124
124
  }
125
125
 
126
126
  function markPluginsCurrent(plugins: PluginInstance[]): void {
@@ -217,7 +217,8 @@ async function runMigrationsWithTimeout(
217
217
  }
218
218
 
219
219
  function resolvePluginMigrationTimeoutMs(): number {
220
- const raw = typeof process !== 'undefined' ? process.env.EDGEBASE_PLUGIN_MIGRATIONS_TIMEOUT_MS : undefined;
220
+ const raw =
221
+ typeof process !== 'undefined' ? process.env.EDGEBASE_PLUGIN_MIGRATIONS_TIMEOUT_MS : undefined;
221
222
  const parsed = Number(raw);
222
223
  if (Number.isFinite(parsed) && parsed > 0) {
223
224
  return parsed;
@@ -250,9 +251,9 @@ async function arePluginMigrationsCurrent(
250
251
  );
251
252
  const versions = new Map(rows.map((row) => [row.key, row.value]));
252
253
 
253
- return versionedPlugins.every((plugin) => (
254
- versions.get(`plugin_version:${plugin.name}`) === plugin.version
255
- ));
254
+ return versionedPlugins.every(
255
+ (plugin) => versions.get(`plugin_version:${plugin.name}`) === plugin.version,
256
+ );
256
257
  }
257
258
 
258
259
  // ─── Helpers ───
@@ -334,6 +335,28 @@ function buildMigrationAdminContext(
334
335
  return directUrl ?? null;
335
336
  }
336
337
 
338
+ const sqlProviderAware = async (
339
+ namespace: string,
340
+ id: string | undefined,
341
+ query: string,
342
+ params?: unknown[],
343
+ ): Promise<unknown[]> => {
344
+ const result = await executeProviderAwareSql(
345
+ {
346
+ env,
347
+ config,
348
+ databaseNamespace: dbNamespace,
349
+ workerUrl,
350
+ serviceKey,
351
+ },
352
+ namespace,
353
+ id,
354
+ query,
355
+ params ?? [],
356
+ );
357
+ return result.rows as unknown[];
358
+ };
359
+
337
360
  return {
338
361
  db(namespace: string, id?: string) {
339
362
  const pgConnStr = resolvePgConnString(namespace);
@@ -350,40 +373,14 @@ function buildMigrationAdminContext(
350
373
  return doAdminDb(namespace, id);
351
374
  },
352
375
 
376
+ sqlProviderAware,
353
377
  async sqlWithDirectD1Access(
354
378
  namespace: string,
355
379
  id: string | undefined,
356
380
  query: string,
357
381
  params?: unknown[],
358
382
  ): Promise<unknown[]> {
359
- const dbBlock = config.databases?.[namespace];
360
- const isDynamicNamespace = !!(dbBlock?.instance || dbBlock?.access?.canCreate || dbBlock?.access?.access);
361
- if (isDynamicNamespace && !id) {
362
- throw new Error(`admin.sqlWithDirectD1Access() requires an id for dynamic namespace '${namespace}'.`);
363
- }
364
-
365
- const pgConnStr = resolvePgConnString(namespace);
366
-
367
- // ─── PostgreSQL path ───
368
- if (pgConnStr) {
369
- // Ensure schema is initialized before raw SQL
370
- const dbBlock = config.databases?.[namespace];
371
- if (dbBlock?.tables) {
372
- await ensurePgSchema(pgConnStr, namespace, dbBlock.tables);
373
- }
374
- const result = await executePostgresQuery(pgConnStr, query, params ?? []);
375
- return result.rows as unknown[];
376
- }
377
-
378
- // ─── DO path (existing) ───
379
- return executeDoSql({
380
- databaseNamespace: dbNamespace,
381
- namespace,
382
- id,
383
- query,
384
- params: params ?? [],
385
- internal: true,
386
- });
383
+ return sqlProviderAware(namespace, id, query, params);
387
384
  },
388
385
 
389
386
  // ─── Convenience shortcut: table(name) → db('shared').table(name) ───
@@ -523,7 +520,8 @@ function buildPgTableOps(
523
520
  const setClauses = (updatableCols.length > 0 ? updatableCols : [conflictTarget]).map(
524
521
  (col) => `${escapePgIdentifier(col)} = EXCLUDED.${escapePgIdentifier(col)}`,
525
522
  );
526
- const sql = `INSERT INTO ${escapePgIdentifier(tableName)} (${cols.map(escapePgIdentifier).join(', ')}) VALUES (${placeholders.join(', ')})` +
523
+ const sql =
524
+ `INSERT INTO ${escapePgIdentifier(tableName)} (${cols.map(escapePgIdentifier).join(', ')}) VALUES (${placeholders.join(', ')})` +
527
525
  ` ON CONFLICT (${escapePgIdentifier(conflictTarget)}) DO UPDATE SET ${setClauses.join(', ')} RETURNING *`;
528
526
  const result = await executePostgresQuery(connectionString, sql, vals);
529
527
  return result.rows[0]
@@ -596,7 +594,9 @@ function buildPgTableOps(
596
594
  }
597
595
 
598
596
  const result = await executePostgresQuery(connectionString, sql, params);
599
- return { items: result.rows.map((row) => stripInternalPgFields(row as Record<string, unknown>)) };
597
+ return {
598
+ items: result.rows.map((row) => stripInternalPgFields(row as Record<string, unknown>)),
599
+ };
600
600
  },
601
601
  };
602
602
  }
@@ -51,6 +51,7 @@ import {
51
51
  import { isTrustedInternalContext } from './internal-request.js';
52
52
  import { executeDbTriggers } from './functions.js';
53
53
  import { parseUpdateBody } from './op-parser.js';
54
+ import { buildTableHookRuntimeServices } from './table-hook-runtime.js';
54
55
 
55
56
  // ─── Types ───
56
57
 
@@ -232,13 +233,14 @@ async function evalInsertRule(
232
233
 
233
234
  function buildHookCtx(
234
235
  connectionString: string,
235
- tables: Record<string, TableConfig>,
236
+ env: Env,
236
237
  executionCtx?: ExecutionContext,
237
238
  queryExecutor?: PostgresExecutor,
238
239
  ): HookCtx {
239
240
  const query =
240
241
  queryExecutor ??
241
242
  ((sql: string, params: unknown[] = []) => executePostgresQuery(connectionString, sql, params));
243
+ const runtimeServices = buildTableHookRuntimeServices(parseConfig(env), env);
242
244
 
243
245
  return {
244
246
  db: {
@@ -279,17 +281,7 @@ function buildHookCtx(
279
281
  return result.rows.length > 0;
280
282
  },
281
283
  },
282
- databaseLive: {
283
- async broadcast(_channel: string, _event: string, _data: unknown): Promise<void> {
284
- // HookCtx broadcast — not implemented for PostgreSQL provider (no direct env access)
285
- // Use database-live subscription from client SDK instead
286
- },
287
- },
288
- push: {
289
- async send(_userId: string, _payload: { title?: string; body: string }): Promise<void> {
290
- // Push notifications — same mechanism as DO (via Worker env)
291
- },
292
- },
284
+ ...runtimeServices,
293
285
  waitUntil(promise: Promise<unknown>): void {
294
286
  if (executionCtx) {
295
287
  executionCtx.waitUntil(promise);
@@ -298,6 +290,18 @@ function buildHookCtx(
298
290
  };
299
291
  }
300
292
 
293
+ function scheduleDbLive(
294
+ executionCtx: ExecutionContext,
295
+ promise: Promise<void>,
296
+ context: string,
297
+ ): void {
298
+ executionCtx.waitUntil(
299
+ promise.catch((error) => {
300
+ console.warn(`[db-live] ${context} failed`, error);
301
+ }),
302
+ );
303
+ }
304
+
301
305
  function toFieldErrorData(
302
306
  errors: Record<string, string>,
303
307
  ): Record<string, { code: string; message: string }> {
@@ -346,7 +350,7 @@ async function handleList(
346
350
 
347
351
  // Apply onEnrich hook
348
352
  if (tableHooks?.onEnrich) {
349
- const hookCtx = buildHookCtx(resolved.connectionString, resolved.dbBlock.tables ?? {}, c.executionCtx, query);
353
+ const hookCtx = buildHookCtx(resolved.connectionString, c.env, c.executionCtx, query);
350
354
  for (let i = 0; i < items.length; i++) {
351
355
  try {
352
356
  const enriched = await tableHooks.onEnrich(auth, items[i], hookCtx);
@@ -495,7 +499,7 @@ async function handleGet(
495
499
 
496
500
  // Apply onEnrich hook
497
501
  if (tableHooks?.onEnrich) {
498
- const hookCtx = buildHookCtx(resolved.connectionString, resolved.dbBlock.tables ?? {}, c.executionCtx, query);
502
+ const hookCtx = buildHookCtx(resolved.connectionString, c.env, c.executionCtx, query);
499
503
  try {
500
504
  const enriched = await tableHooks.onEnrich(auth, row, hookCtx);
501
505
  if (enriched && typeof enriched === 'object') return c.json({ ...row, ...enriched });
@@ -546,7 +550,7 @@ async function handleInsert(
546
550
  }
547
551
 
548
552
  // Run beforeInsert hook
549
- const requestHookCtx = buildHookCtx(resolved.connectionString, resolved.dbBlock.tables ?? {}, c.executionCtx, query);
553
+ const requestHookCtx = buildHookCtx(resolved.connectionString, c.env, c.executionCtx, query);
550
554
  if (tableHooks?.beforeInsert) {
551
555
  try {
552
556
  const transformed = await tableHooks.beforeInsert(auth, body, requestHookCtx);
@@ -600,12 +604,13 @@ async function handleInsert(
600
604
  // Run afterInsert hook (fire-and-forget)
601
605
  if (tableHooks?.afterInsert) {
602
606
  const hook = tableHooks.afterInsert;
603
- const backgroundHookCtx = buildHookCtx(resolved.connectionString, resolved.dbBlock.tables ?? {}, c.executionCtx);
607
+ const backgroundHookCtx = buildHookCtx(resolved.connectionString, c.env, c.executionCtx);
604
608
  backgroundHookCtx.waitUntil(Promise.resolve(hook(inserted, backgroundHookCtx)).catch(() => {}));
605
609
  }
606
610
 
607
611
  // Emit database-live event (fire-and-forget)
608
- c.executionCtx.waitUntil(
612
+ scheduleDbLive(
613
+ c.executionCtx,
609
614
  emitDbLiveEvent(
610
615
  c.env,
611
616
  resolved.namespace,
@@ -614,6 +619,7 @@ async function handleInsert(
614
619
  String(inserted.id ?? ''),
615
620
  inserted,
616
621
  ),
622
+ `emit ${isUpsert && isUpdate ? 'modified' : 'added'} ${resolved.namespace}.${tableName}`,
617
623
  );
618
624
  c.executionCtx.waitUntil(
619
625
  executeDbTriggers(
@@ -689,7 +695,7 @@ async function handleUpdate(
689
695
  }
690
696
 
691
697
  // Run beforeUpdate hook
692
- const requestHookCtx = buildHookCtx(resolved.connectionString, resolved.dbBlock.tables ?? {}, c.executionCtx, query);
698
+ const requestHookCtx = buildHookCtx(resolved.connectionString, c.env, c.executionCtx, query);
693
699
  if (tableHooks?.beforeUpdate) {
694
700
  try {
695
701
  const transformed = await tableHooks.beforeUpdate(auth, existingRow, body, requestHookCtx);
@@ -726,15 +732,17 @@ async function handleUpdate(
726
732
  // Run afterUpdate hook (fire-and-forget)
727
733
  if (tableHooks?.afterUpdate) {
728
734
  const hook = tableHooks.afterUpdate;
729
- const backgroundHookCtx = buildHookCtx(resolved.connectionString, resolved.dbBlock.tables ?? {}, c.executionCtx);
735
+ const backgroundHookCtx = buildHookCtx(resolved.connectionString, c.env, c.executionCtx);
730
736
  backgroundHookCtx.waitUntil(
731
737
  Promise.resolve(hook(existingRow, updated, backgroundHookCtx)).catch(() => {}),
732
738
  );
733
739
  }
734
740
 
735
741
  // Emit database-live event (fire-and-forget)
736
- c.executionCtx.waitUntil(
742
+ scheduleDbLive(
743
+ c.executionCtx,
737
744
  emitDbLiveEvent(c.env, resolved.namespace, tableName, 'modified', id, updated),
745
+ `emit modified ${resolved.namespace}.${tableName}:${id}`,
738
746
  );
739
747
  c.executionCtx.waitUntil(
740
748
  executeDbTriggers(
@@ -789,7 +797,7 @@ async function handleDelete(
789
797
  }
790
798
 
791
799
  // Run beforeDelete hook
792
- const requestHookCtx = buildHookCtx(resolved.connectionString, resolved.dbBlock.tables ?? {}, c.executionCtx, query);
800
+ const requestHookCtx = buildHookCtx(resolved.connectionString, c.env, c.executionCtx, query);
793
801
  if (tableHooks?.beforeDelete) {
794
802
  try {
795
803
  await tableHooks.beforeDelete(auth, existingRow, requestHookCtx);
@@ -810,15 +818,17 @@ async function handleDelete(
810
818
  // Run afterDelete hook (fire-and-forget)
811
819
  if (tableHooks?.afterDelete) {
812
820
  const hook = tableHooks.afterDelete;
813
- const backgroundHookCtx = buildHookCtx(resolved.connectionString, resolved.dbBlock.tables ?? {}, c.executionCtx);
821
+ const backgroundHookCtx = buildHookCtx(resolved.connectionString, c.env, c.executionCtx);
814
822
  backgroundHookCtx.waitUntil(
815
823
  Promise.resolve(hook(existingRow, backgroundHookCtx)).catch(() => {}),
816
824
  );
817
825
  }
818
826
 
819
827
  // Emit database-live event (fire-and-forget)
820
- c.executionCtx.waitUntil(
828
+ scheduleDbLive(
829
+ c.executionCtx,
821
830
  emitDbLiveEvent(c.env, resolved.namespace, tableName, 'removed', id, stripInternalPgFields(existingRow)),
831
+ `emit removed ${resolved.namespace}.${tableName}:${id}`,
822
832
  );
823
833
  c.executionCtx.waitUntil(
824
834
  executeDbTriggers(
@@ -944,15 +954,21 @@ async function handleBatch(
944
954
  data: r as Record<string, unknown>,
945
955
  }));
946
956
  if (changes.length >= 10) {
947
- c.executionCtx.waitUntil(
957
+ scheduleDbLive(
958
+ c.executionCtx,
948
959
  emitDbLiveBatchEvent(c.env, resolved.namespace, tableName, changes),
960
+ `emit batch ${resolved.namespace}.${tableName} (${changes.length} changes)`,
949
961
  );
950
962
  } else {
951
- for (const ch of changes) {
952
- c.executionCtx.waitUntil(
953
- emitDbLiveEvent(c.env, resolved.namespace, tableName, ch.type, ch.docId, ch.data),
954
- );
955
- }
963
+ scheduleDbLive(
964
+ c.executionCtx,
965
+ Promise.all(
966
+ changes.map((ch) =>
967
+ emitDbLiveEvent(c.env, resolved.namespace, tableName, ch.type, ch.docId, ch.data),
968
+ ),
969
+ ).then(() => undefined),
970
+ `emit fan-out ${resolved.namespace}.${tableName} (${changes.length} changes)`,
971
+ );
956
972
  }
957
973
  }
958
974
 
@@ -1031,8 +1047,10 @@ async function handleBatchByFilter(
1031
1047
  succeeded = result.rowCount;
1032
1048
 
1033
1049
  if (succeeded > 0) {
1034
- c.executionCtx.waitUntil(
1050
+ scheduleDbLive(
1051
+ c.executionCtx,
1035
1052
  emitDbLiveEvent(c.env, resolved.namespace, tableName, 'removed', '_bulk', { action: 'delete', count: succeeded }),
1053
+ `emit bulk ${resolved.namespace}.${tableName} (delete)`,
1036
1054
  );
1037
1055
  }
1038
1056
 
@@ -1074,8 +1092,10 @@ async function handleBatchByFilter(
1074
1092
  succeeded = result.rowCount;
1075
1093
 
1076
1094
  if (succeeded > 0) {
1077
- c.executionCtx.waitUntil(
1095
+ scheduleDbLive(
1096
+ c.executionCtx,
1078
1097
  emitDbLiveEvent(c.env, resolved.namespace, tableName, 'modified', '_bulk', { action: 'update', count: succeeded }),
1098
+ `emit bulk ${resolved.namespace}.${tableName} (update)`,
1079
1099
  );
1080
1100
  }
1081
1101