@lunora/do 1.0.0-alpha.14 → 1.0.0-alpha.15

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.
package/dist/index.d.mts CHANGED
@@ -5536,6 +5536,26 @@ declare abstract class ShardDO {
5536
5536
  */
5537
5537
  private errorToResponse;
5538
5538
  /**
5539
+ * Batch dispatch (plan 088). Applies each `calls[]` entry through the SAME
5540
+ * single-call `/rpc` path (via a nested `this.fetch`), **sequentially**, so
5541
+ * the per-`(identity, mutationId)` idempotency dedup and the per-client
5542
+ * `__client_watermark` ordering are enforced entry-by-entry exactly as for an
5543
+ * individual call — no duplication of the dispatch core, no reordering.
5544
+ *
5545
+ * Failures are **per-slot, not fail-fast**: an entry that throws (or a
5546
+ * custom-mutator `OUT_OF_ORDER` gap) is captured in its own result slot and
5547
+ * later entries still run. Ordering is still safe — a later same-client
5548
+ * mutator after a gap re-classifies as a gap too (the watermark never
5549
+ * advanced), so it cannot apply out of order; unrelated entries/queries are
5550
+ * independent. The response is `{ results: [{ id, status, body }] }` in
5551
+ * request order; each `body` is the untouched single-call envelope (its
5552
+ * `result` already wire-encoded), so the client demuxes + decodes each
5553
+ * exactly as one call.
5554
+ */
5555
+ private handleBatchRpc;
5556
+ /** Dispatch one batch entry through the single-call `/rpc` path and capture its envelope (plan 088). */
5557
+ private dispatchBatchEntry;
5558
+ /**
5539
5559
  * Serve a reserved admin-introspection RPC (`__lunora_admin__:*`) for the
5540
5560
  * data browser. Gated by `env.LUNORA_ADMIN_TOKEN`: introspection is
5541
5561
  * **disabled unless the token is configured**, and when it is, the request
package/dist/index.d.ts CHANGED
@@ -5536,6 +5536,26 @@ declare abstract class ShardDO {
5536
5536
  */
5537
5537
  private errorToResponse;
5538
5538
  /**
5539
+ * Batch dispatch (plan 088). Applies each `calls[]` entry through the SAME
5540
+ * single-call `/rpc` path (via a nested `this.fetch`), **sequentially**, so
5541
+ * the per-`(identity, mutationId)` idempotency dedup and the per-client
5542
+ * `__client_watermark` ordering are enforced entry-by-entry exactly as for an
5543
+ * individual call — no duplication of the dispatch core, no reordering.
5544
+ *
5545
+ * Failures are **per-slot, not fail-fast**: an entry that throws (or a
5546
+ * custom-mutator `OUT_OF_ORDER` gap) is captured in its own result slot and
5547
+ * later entries still run. Ordering is still safe — a later same-client
5548
+ * mutator after a gap re-classifies as a gap too (the watermark never
5549
+ * advanced), so it cannot apply out of order; unrelated entries/queries are
5550
+ * independent. The response is `{ results: [{ id, status, body }] }` in
5551
+ * request order; each `body` is the untouched single-call envelope (its
5552
+ * `result` already wire-encoded), so the client demuxes + decodes each
5553
+ * exactly as one call.
5554
+ */
5555
+ private handleBatchRpc;
5556
+ /** Dispatch one batch entry through the single-call `/rpc` path and capture its envelope (plan 088). */
5557
+ private dispatchBatchEntry;
5558
+ /**
5539
5559
  * Serve a reserved admin-introspection RPC (`__lunora_admin__:*`) for the
5540
5560
  * data browser. Gated by `env.LUNORA_ADMIN_TOKEN`: introspection is
5541
5561
  * **disabled unless the token is configured**, and when it is, the request
package/dist/index.mjs CHANGED
@@ -7,9 +7,9 @@ export { NotUniqueError, assertValidClientId, createShardCtxDb, normalizeIdStruc
7
7
  export { DATA_MIGRATION_STATE_TABLE, readMigrationStatus, runDataMigration } from './packem_shared/DATA_MIGRATION_STATE_TABLE-PTtTiQ7U.mjs';
8
8
  export { SCAN_DEP, createDependencyTracker, depKey } from './packem_shared/SCAN_DEP-DLJF8dsj.mjs';
9
9
  export { renderSql } from './packem_shared/renderSql-D6eUcn2N.mjs';
10
- export { diffExternalSource } from './packem_shared/diffExternalSource-Br2GYVZB.mjs';
11
- export { materializeExternalRows, readExternalSourceBaseline, runExternalSourceTick } from './packem_shared/materializeExternalRows-CcsfmiDe.mjs';
12
- export { isSourceDue, liftSourceId, pullExternalSourceTick } from './packem_shared/isSourceDue-D_wTv871.mjs';
10
+ export { diffExternalSource } from './packem_shared/diffExternalSource-Cx9HUPJj.mjs';
11
+ export { materializeExternalRows, readExternalSourceBaseline, runExternalSourceTick } from './packem_shared/materializeExternalRows-uKBx9H0s.mjs';
12
+ export { isSourceDue, liftSourceId, pullExternalSourceTick } from './packem_shared/isSourceDue-BMTJv65e.mjs';
13
13
  export { FUNCTION_METRICS_BUCKETS_TABLE, FUNCTION_METRICS_BUCKET_MS, FUNCTION_METRICS_BUCKET_RETENTION, FUNCTION_METRICS_INDEX_TABLE, FUNCTION_METRICS_TABLE, ensureFunctionMetricsTables, readFunctionMetricBuckets, readFunctionMetricIndexHits, readFunctionMetrics, readFunctionMetricsTotals, recordFunctionMetric } from './packem_shared/FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs';
14
14
  export { ADMIN_FUNCTIONS, ADMIN_FUNCTION_PREFIX, FLAGS_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, facetColumn, listTables, readTablePage, selectMatchingIds } from './packem_shared/ADMIN_FUNCTIONS-fMl1vyt1.mjs';
15
15
  export { LogBuffer } from './packem_shared/LogBuffer-B_Ezju_N.mjs';
@@ -18,7 +18,7 @@ export { default as NotFoundError } from './packem_shared/NotFoundError-CMuMZt81
18
18
  export { armRestore, readBookmark } from './packem_shared/armRestore-BJk53Ro8.mjs';
19
19
  export { applySelect, buildSeekWhere, decodeCursor, encodeCursor, normalizeOrderKeys, softDeleteScope } from './packem_shared/applySelect-BvZdFUBT.mjs';
20
20
  export { RANK_TIEBREAK, encodePartitionKey, matchesRankStaticWhere, rankTableName, resolveRankPartition, sortColumnName } from './packem_shared/RANK_TIEBREAK-CXhdcA1o.mjs';
21
- export { ReactiveCache, reactiveCacheKey } from './packem_shared/ReactiveCache-1hDydFyv.mjs';
21
+ export { ReactiveCache, reactiveCacheKey } from './packem_shared/ReactiveCache-BYlSGY0N.mjs';
22
22
  export { serveRelationFanout } from './packem_shared/serveRelationFanout-EZLD9-O3.mjs';
23
23
  export { DEFAULT_MAX_RELATION_KEYS, assertFlatPredicate, assertShapeShardable, containsRelationPredicate, isRelationPredicate, resolveRelationPredicates } from './packem_shared/DEFAULT_MAX_RELATION_KEYS-DU-Y4-LJ.mjs';
24
24
  export { applyOnDelete, fanOutScalarCounts, resolveWith, runRowValidators } from './packem_shared/applyOnDelete-BQ-8ZlZ1.mjs';
@@ -26,7 +26,7 @@ export { RLS_UNWRAP_SYMBOL, RlsRequiredError, guardWriter } from './packem_share
26
26
  export { buildFtsMatch, ftsTableName, scoreDocument, stringifySearchText, tokenizeSearch } from './packem_shared/buildFtsMatch-BLEMawrp.mjs';
27
27
  export { M as MIN_ADMIN_TOKEN_LENGTH, a as MIN_AUTH_SECRET_LENGTH, b as buildSecurityAudit } from './packem_shared/security-audit-CucgBice.mjs';
28
28
  export { SESSION_DO_TTL_DEFAULT, SessionDO } from './packem_shared/SESSION_DO_TTL_DEFAULT-ilPZsVwu.mjs';
29
- export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-DXVOGLBS.mjs';
29
+ export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-CEVhBec2.mjs';
30
30
  export { SHARD_REGISTRY_DO_NAME, ShardRegistryDO } from './packem_shared/SHARD_REGISTRY_DO_NAME-BsAbi5Mn.mjs';
31
31
  export { MAX_SQL_ROWS, assertReadonly, runReadonlySql } from './packem_shared/MAX_SQL_ROWS-dDcFE1YZ.mjs';
32
32
  export { createSystemReader } from './packem_shared/createSystemReader-8CzSZP9V.mjs';
@@ -36,5 +36,5 @@ export { compileWhereSql } from './packem_shared/compileWhereSql-CXrhFA3G.mjs';
36
36
  export { CDC_LOG_TABLE, applyCdcChanges, readCdcChanges, trimCdcChanges } from './packem_shared/CDC_LOG_TABLE-DSycmnDf.mjs';
37
37
  export { backfillAggregateIndexes, backfillRankIndexes } from './packem_shared/backfillAggregateIndexes-BZsOqDXP.mjs';
38
38
  export { runShardMigrations } from './packem_shared/runShardMigrations-nIwoQeOK.mjs';
39
- export { stableStringify } from './packem_shared/stableStringify-CyHKJXre.mjs';
40
- export { subscriptionListDeltas } from './packem_shared/subscriptionListDeltas-DoB136JT.mjs';
39
+ export { stableStringify } from './packem_shared/stableStringify-MydiuScU.mjs';
40
+ export { s as subscriptionListDeltas } from './packem_shared/subscription-delivery-Z6nXHvQN.mjs';
@@ -1,4 +1,5 @@
1
1
  import { drizzle } from 'drizzle-orm/durable-sqlite';
2
+ import { e as encodeWire, a as awaitWsDrain, t as trySendFrame, d as decodeWire, s as subscriptionListDeltas, b as sendDeltaFrames } from './subscription-delivery-Z6nXHvQN.mjs';
2
3
  import { parseExportShardArgs, parseImportShardArgs } from './exportShardRows-DZEhUeyI.mjs';
3
4
  import { recordAuthEvent, readAuthMetrics } from './AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs';
4
5
  import { DATA_MIGRATION_STATE_TABLE, readMigrationStatus } from './DATA_MIGRATION_STATE_TABLE-PTtTiQ7U.mjs';
@@ -8,9 +9,8 @@ import { createFanoutCounters, ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX,
8
9
  import { LogBuffer } from './LogBuffer-B_Ezju_N.mjs';
9
10
  import { recordCapturedMail, clearCapturedMail, readCapturedMail, MAIL_TABLE } from './MAIL_RETENTION-CPpgl-dX.mjs';
10
11
  import { readBookmark, armRestore } from './armRestore-BJk53Ro8.mjs';
11
- import { ReactiveCache, reactiveCacheKey } from './ReactiveCache-1hDydFyv.mjs';
12
- import { stableStringify } from './stableStringify-CyHKJXre.mjs';
13
- import { awaitWsDrain, trySendFrame, subscriptionListDeltas, sendDeltaFrames } from './subscriptionListDeltas-DoB136JT.mjs';
12
+ import { ReactiveCache, reactiveCacheKey } from './ReactiveCache-BYlSGY0N.mjs';
13
+ import { stableStringify } from './stableStringify-MydiuScU.mjs';
14
14
  import { redact, standardRules } from '@visulima/redact';
15
15
  import { i as isDevEnvironment, c as buildSettings, b as buildSecurityAudit } from './security-audit-CucgBice.mjs';
16
16
  import { runReadonlySql } from './MAX_SQL_ROWS-dDcFE1YZ.mjs';
@@ -19,6 +19,8 @@ import { e as deleteGlobalShapeSnapshotsForConnection, g as readIdempotent, h as
19
19
  import { CDC_LOG_TABLE, readCdcChanges, readCdcCursor, readCdcEpoch, minCdcSeq, bumpCdcEpoch } from './CDC_LOG_TABLE-DSycmnDf.mjs';
20
20
  import { a as selectShapeMemberIds, s as selectShapeRows } from './ctx-db-shapes-BQapRFjB.mjs';
21
21
 
22
+ const MAX_BATCH_ENTRIES = 500;
23
+
22
24
  const AUDIT_LOG_TABLE = "__lunora_audit__";
23
25
  const AUDIT_LOG_RETENTION = 1e3;
24
26
  const runSql$2 = (sql, query, ...params) => {
@@ -79,6 +81,38 @@ const readAuditLog = (sql, options = {}) => {
79
81
  });
80
82
  };
81
83
 
84
+ const SHARED_BATCH_HEADERS = [
85
+ "x-lunora-userid",
86
+ "x-lunora-identity",
87
+ "x-d1-bookmark",
88
+ "x-lunora-client-ip",
89
+ "x-lunora-system",
90
+ "x-lunora-shard-binding"
91
+ ];
92
+ const buildBatchEntryRequest = (batchRequest, entry) => {
93
+ const headers = new Headers({ "content-type": "application/json" });
94
+ for (const name of SHARED_BATCH_HEADERS) {
95
+ const value = batchRequest.headers.get(name);
96
+ if (value !== null) {
97
+ headers.set(name, value);
98
+ }
99
+ }
100
+ if (entry.mutationId !== void 0) {
101
+ headers.set("x-lunora-mutation-id", entry.mutationId);
102
+ }
103
+ if (entry.clientId !== void 0) {
104
+ headers.set("x-lunora-client-id", entry.clientId);
105
+ }
106
+ if (entry.clientSeq !== void 0) {
107
+ headers.set("x-lunora-client-seq", String(entry.clientSeq));
108
+ }
109
+ return new Request("https://shard.internal/rpc", {
110
+ body: JSON.stringify({ args: entry.args ?? {}, functionPath: entry.functionPath }),
111
+ headers,
112
+ method: "POST"
113
+ });
114
+ };
115
+
82
116
  const QUERY_METRICS_TABLE = "__lunora_metrics_queries";
83
117
  const QUERY_METRICS_MAX_SQL_LEN = 512;
84
118
  const QUERY_METRICS_MAX_STATEMENTS = 500;
@@ -182,7 +216,7 @@ const diffGlobalMembership = (rows, previous, options) => {
182
216
  const rowsPatch = [];
183
217
  for (const { doc, id } of rows) {
184
218
  const value = projectColumns(doc, columns);
185
- const json = JSON.stringify(value);
219
+ const json = JSON.stringify(encodeWire(value));
186
220
  next.set(id, json);
187
221
  const before = previous.get(id);
188
222
  if (before === void 0) {
@@ -202,10 +236,11 @@ const buildPokeFrames = (parts, meta) => {
202
236
  const { baseCheckpoint, checkpoint, epoch, lastMutationId, pokeId } = meta;
203
237
  const frames = [JSON.stringify({ baseCheckpoint, epoch, pokeId, type: "pokeStart" })];
204
238
  for (const part of parts) {
239
+ const rowsPatch = part.rowsPatch.map((op) => op.value === void 0 ? op : { ...op, value: encodeWire(op.value) });
205
240
  frames.push(
206
241
  JSON.stringify({
207
242
  pokeId,
208
- rowsPatch: part.rowsPatch,
243
+ rowsPatch,
209
244
  shapeId: part.shapeId,
210
245
  type: "pokePart",
211
246
  ...lastMutationId === void 0 ? {} : { lastMutationId }
@@ -2086,7 +2121,7 @@ class ShardDO {
2086
2121
  if (cached !== void 0) {
2087
2122
  return this.respondFromIdempotencyCache(payload.functionPath, dispatchStartedAt, mutatorClass, cached.value);
2088
2123
  }
2089
- const result = await this.handleRpc(payload.functionPath, payload.args ?? {});
2124
+ const result = await this.handleRpc(payload.functionPath, decodeWire(payload.args ?? {}));
2090
2125
  this.recordPostDispatchBookkeeping(result, mutatorClass);
2091
2126
  if (mutatorClass?.kind === "next") {
2092
2127
  this.advanceClientMutationWatermark();
@@ -2097,7 +2132,7 @@ class ShardDO {
2097
2132
  const tablesWritten = [...this.pendingChangedTables ?? []];
2098
2133
  this.recordRequestLog(payload.functionPath, payload.args ?? {}, durationMs, "ok", tablesWritten);
2099
2134
  this.maybeWarnRootSize();
2100
- const response = this.buildDispatchResponse(mutatorClass, result);
2135
+ const response = this.buildDispatchResponse(mutatorClass, encodeWire(result));
2101
2136
  await this.flushChangedTables();
2102
2137
  return response;
2103
2138
  } catch (error) {
@@ -2217,7 +2252,7 @@ class ShardDO {
2217
2252
  ws.send(JSON.stringify({ id: envelope.id, message: "streams must be public", type: "error" }));
2218
2253
  return;
2219
2254
  }
2220
- this.handleStream(ws, envelope.id, envelope.query.functionPath, envelope.query.args ?? {}).catch(() => {
2255
+ this.handleStream(ws, envelope.id, envelope.query.functionPath, decodeWire(envelope.query.args ?? {})).catch(() => {
2221
2256
  });
2222
2257
  return;
2223
2258
  }
@@ -3031,7 +3066,7 @@ class ShardDO {
3031
3066
  }
3032
3067
  const now = Date.now();
3033
3068
  try {
3034
- writeIdempotent(this.sql, this.currentRequestUserId ?? "", this.currentRequestMutationId, JSON.stringify(result) ?? "null", now);
3069
+ writeIdempotent(this.sql, this.currentRequestUserId ?? "", this.currentRequestMutationId, JSON.stringify(encodeWire(result)), now);
3035
3070
  if (now - this.lastIdempotencyTrimAt > IDEMPOTENCY_GC_INTERVAL_MS) {
3036
3071
  trimIdempotent(this.sql, now - IDEMPOTENCY_RETENTION_MS);
3037
3072
  this.lastIdempotencyTrimAt = now;
@@ -3887,11 +3922,74 @@ class ShardDO {
3887
3922
  if (error && typeof error === "object" && error.name === "LunoraError") {
3888
3923
  const lunoraError = error;
3889
3924
  const status = typeof lunoraError.status === "number" ? lunoraError.status : 500;
3890
- return jsonResponse({ error: { code: lunoraError.code ?? "INTERNAL", message: lunoraError.message ?? "internal error" } }, status);
3925
+ const body = {
3926
+ code: lunoraError.code ?? "INTERNAL",
3927
+ message: lunoraError.message ?? "internal error"
3928
+ };
3929
+ if (lunoraError.data !== void 0) {
3930
+ body.data = encodeWire(lunoraError.data);
3931
+ }
3932
+ return jsonResponse({ error: body }, status);
3891
3933
  }
3892
3934
  console.error("[@lunora/do] unhandled RPC error:", error);
3893
3935
  return jsonResponse({ error: { code: "RPC_FAILED", message: "internal error" } }, 500);
3894
3936
  }
3937
+ /**
3938
+ * Batch dispatch (plan 088). Applies each `calls[]` entry through the SAME
3939
+ * single-call `/rpc` path (via a nested `this.fetch`), **sequentially**, so
3940
+ * the per-`(identity, mutationId)` idempotency dedup and the per-client
3941
+ * `__client_watermark` ordering are enforced entry-by-entry exactly as for an
3942
+ * individual call — no duplication of the dispatch core, no reordering.
3943
+ *
3944
+ * Failures are **per-slot, not fail-fast**: an entry that throws (or a
3945
+ * custom-mutator `OUT_OF_ORDER` gap) is captured in its own result slot and
3946
+ * later entries still run. Ordering is still safe — a later same-client
3947
+ * mutator after a gap re-classifies as a gap too (the watermark never
3948
+ * advanced), so it cannot apply out of order; unrelated entries/queries are
3949
+ * independent. The response is `{ results: [{ id, status, body }] }` in
3950
+ * request order; each `body` is the untouched single-call envelope (its
3951
+ * `result` already wire-encoded), so the client demuxes + decodes each
3952
+ * exactly as one call.
3953
+ */
3954
+ async handleBatchRpc(request) {
3955
+ let payload;
3956
+ try {
3957
+ payload = await request.json();
3958
+ } catch {
3959
+ return jsonResponse({ error: { code: "BAD_REQUEST", message: "invalid JSON body" } }, 400);
3960
+ }
3961
+ if (!Array.isArray(payload.calls)) {
3962
+ return jsonResponse({ error: { code: "BAD_REQUEST", message: "batch `calls` must be an array" } }, 400);
3963
+ }
3964
+ if (payload.calls.length > MAX_BATCH_ENTRIES) {
3965
+ return jsonResponse({ error: { code: "BAD_REQUEST", message: `batch exceeds the ${String(MAX_BATCH_ENTRIES)}-call limit` } }, 400);
3966
+ }
3967
+ const results = [];
3968
+ let latestBookmark;
3969
+ for (const raw of payload.calls) {
3970
+ const outcome = await this.dispatchBatchEntry(request, raw);
3971
+ if (outcome.bookmark !== void 0) {
3972
+ latestBookmark = outcome.bookmark;
3973
+ }
3974
+ results.push({ body: outcome.body, id: outcome.id, status: outcome.status });
3975
+ }
3976
+ return jsonResponse({ results }, 200, latestBookmark);
3977
+ }
3978
+ /** Dispatch one batch entry through the single-call `/rpc` path and capture its envelope (plan 088). */
3979
+ async dispatchBatchEntry(batchRequest, entry) {
3980
+ try {
3981
+ const response = await this.fetch(buildBatchEntryRequest(batchRequest, entry));
3982
+ return { body: await response.json(), bookmark: response.headers.get("x-d1-bookmark") ?? void 0, id: entry.id, status: response.status };
3983
+ } catch (error) {
3984
+ const message = error instanceof Error ? error.message : String(error);
3985
+ return {
3986
+ body: { error: { code: "BATCH_ENTRY_FAILED", message } },
3987
+ bookmark: void 0,
3988
+ id: entry?.id,
3989
+ status: 500
3990
+ };
3991
+ }
3992
+ }
3895
3993
  /**
3896
3994
  * Serve a reserved admin-introspection RPC (`__lunora_admin__:*`) for the
3897
3995
  * data browser. Gated by `env.LUNORA_ADMIN_TOKEN`: introspection is
@@ -4841,7 +4939,7 @@ class ShardDO {
4841
4939
  break;
4842
4940
  }
4843
4941
  await awaitWsDrain(ws);
4844
- ws.send(JSON.stringify({ data: chunk, id, type: "chunk" }));
4942
+ ws.send(JSON.stringify({ data: encodeWire(chunk), id, type: "chunk" }));
4845
4943
  }
4846
4944
  if (!controller.signal.aborted) {
4847
4945
  ws.send(JSON.stringify({ id, type: "complete" }));
@@ -5634,7 +5732,7 @@ class ShardDO {
5634
5732
  memos = /* @__PURE__ */ new Map();
5635
5733
  this.subMemos.set(ws, memos);
5636
5734
  }
5637
- memos.set(subId, { lastJson: JSON.stringify(outcome.result ?? null), tables: outcome.tables });
5735
+ memos.set(subId, { lastJson: JSON.stringify(encodeWire(outcome.result ?? null)), tables: outcome.tables });
5638
5736
  }
5639
5737
  /**
5640
5738
  * Memoise `outcome` for `(ws, subId)` and push it to the socket, unless an
@@ -5661,7 +5759,7 @@ class ShardDO {
5661
5759
  this.subMemos.set(ws, memos);
5662
5760
  }
5663
5761
  const cursorSuffix = cdcSuffix(cursor, epoch);
5664
- const json = JSON.stringify(outcome.result ?? null);
5762
+ const json = JSON.stringify(encodeWire(outcome.result ?? null));
5665
5763
  const existing = memos.get(subId);
5666
5764
  if (existing?.lastJson === json) {
5667
5765
  existing.tables = outcome.tables;
@@ -5778,6 +5876,9 @@ class ShardDO {
5778
5876
  if (url.pathname === "/_lunora/route" && request.method === "GET") {
5779
5877
  return jsonResponse({ relayCount: this.relay?.relayCount() ?? 0 });
5780
5878
  }
5879
+ if (url.pathname === "/rpc-batch" && request.method === "POST") {
5880
+ return this.handleBatchRpc(request);
5881
+ }
5781
5882
  if (request.headers.get("Upgrade") === "websocket") {
5782
5883
  return this.handleWebSocketUpgrade(request);
5783
5884
  }
@@ -1,4 +1,4 @@
1
- import { stableStringify } from './stableStringify-CyHKJXre.mjs';
1
+ import { stableStringify } from './stableStringify-MydiuScU.mjs';
2
2
  import { depKey, SCAN_DEP } from './SCAN_DEP-DLJF8dsj.mjs';
3
3
 
4
4
  const DEFAULT_MAX_ENTRIES = 1e3;
@@ -1,4 +1,4 @@
1
- import { stableStringify } from './stableStringify-CyHKJXre.mjs';
1
+ import { stableStringify } from './stableStringify-MydiuScU.mjs';
2
2
 
3
3
  const projectExternalSourceRow = (row, columns) => {
4
4
  const projected = { _id: row._id };
@@ -1,4 +1,4 @@
1
- import { runExternalSourceTick } from './materializeExternalRows-CcsfmiDe.mjs';
1
+ import { runExternalSourceTick } from './materializeExternalRows-uKBx9H0s.mjs';
2
2
 
3
3
  const liftSourceId = (row, options = {}) => {
4
4
  const { idColumn = "id", map } = options;
@@ -1,7 +1,7 @@
1
1
  import { applyCdcChanges } from './CDC_LOG_TABLE-DSycmnDf.mjs';
2
2
  import { s as selectShapeRows } from './ctx-db-shapes-BQapRFjB.mjs';
3
- import { diffExternalSource, projectExternalSourceRow } from './diffExternalSource-Br2GYVZB.mjs';
4
- import { stableStringify } from './stableStringify-CyHKJXre.mjs';
3
+ import { diffExternalSource, projectExternalSourceRow } from './diffExternalSource-Cx9HUPJj.mjs';
4
+ import { stableStringify } from './stableStringify-MydiuScU.mjs';
5
5
 
6
6
  const materializeExternalRows = async (writer, pulled, baseline, options) => {
7
7
  const { changes, nextBaseline } = diffExternalSource(pulled, baseline, options);
@@ -8,12 +8,22 @@ const stableStringify = (value) => {
8
8
  if (value === void 0) {
9
9
  return "null";
10
10
  }
11
+ if (typeof value === "bigint") {
12
+ throw new TypeError("stableStringify: cannot use a bigint in a cache key (query/subscription/shape args) — pass it as a string");
13
+ }
11
14
  if (value === null || typeof value !== "object") {
12
15
  return JSON.stringify(value);
13
16
  }
14
17
  if (Array.isArray(value)) {
15
18
  return `[${value.map((item) => stableStringify(item)).join(",")}]`;
16
19
  }
20
+ const proto = Object.getPrototypeOf(value);
21
+ if (proto !== null && proto !== Object.prototype) {
22
+ const name = value.constructor?.name ?? "value";
23
+ throw new TypeError(
24
+ `stableStringify: cannot use a ${name} in a cache key (query/subscription/shape args) — only plain objects, arrays, and JSON primitives are supported`
25
+ );
26
+ }
17
27
  const record = value;
18
28
  const keys = Object.keys(record).toSorted(compareKeys);
19
29
  const parts = [];
@@ -0,0 +1,340 @@
1
+ const TAG = "$lunora.wire$";
2
+ const MAX_DEPTH = 64;
3
+ const MAX_BIGINT_DIGITS = 1024;
4
+ const UNSAFE_KEY = "__proto__";
5
+ const TYPED_ARRAY_CTORS = {
6
+ BigInt64Array,
7
+ BigUint64Array,
8
+ Float32Array,
9
+ Float64Array,
10
+ Int8Array,
11
+ Int16Array,
12
+ Int32Array,
13
+ Uint8Array,
14
+ Uint8ClampedArray,
15
+ Uint16Array,
16
+ Uint32Array
17
+ };
18
+ const ERROR_CTORS = {
19
+ Error,
20
+ EvalError,
21
+ RangeError,
22
+ ReferenceError,
23
+ SyntaxError,
24
+ TypeError,
25
+ URIError
26
+ };
27
+ const toBase64 = (bytes) => {
28
+ let binary = "";
29
+ const chunk = 32768;
30
+ for (let index = 0; index < bytes.length; index += chunk) {
31
+ binary += String.fromCharCode(...bytes.subarray(index, index + chunk));
32
+ }
33
+ return btoa(binary);
34
+ };
35
+ const fromBase64 = (base64) => {
36
+ const binary = atob(base64);
37
+ const bytes = new Uint8Array(binary.length);
38
+ for (let index = 0; index < binary.length; index += 1) {
39
+ bytes[index] = binary.codePointAt(index) ?? 0;
40
+ }
41
+ return bytes;
42
+ };
43
+ const encodeWire = (value, depth = 0) => {
44
+ if (depth > MAX_DEPTH) {
45
+ throw new RangeError(`wire-codec: value nesting exceeds the ${MAX_DEPTH}-level limit`);
46
+ }
47
+ if (value === void 0) {
48
+ return [TAG, "undefined"];
49
+ }
50
+ if (value === null) {
51
+ return null;
52
+ }
53
+ const kind = typeof value;
54
+ if (kind === "bigint") {
55
+ return [TAG, "bigint", value.toString()];
56
+ }
57
+ if (kind === "number") {
58
+ const numeric = value;
59
+ if (Number.isNaN(numeric)) {
60
+ return [TAG, "nan"];
61
+ }
62
+ if (numeric === Infinity) {
63
+ return [TAG, "inf"];
64
+ }
65
+ if (numeric === -Infinity) {
66
+ return [TAG, "-inf"];
67
+ }
68
+ return numeric;
69
+ }
70
+ if (kind !== "object") {
71
+ return value;
72
+ }
73
+ if (value instanceof Date) {
74
+ return [TAG, "date", encodeWire(value.getTime(), depth + 1)];
75
+ }
76
+ if (value instanceof Error) {
77
+ const error = value;
78
+ const properties = {};
79
+ for (const key of Object.keys(error)) {
80
+ if (error[key] !== void 0) {
81
+ properties[key] = encodeWire(error[key], depth + 1);
82
+ }
83
+ }
84
+ const encodedError = [TAG, "error", error.name, error.message, properties];
85
+ if (error.cause !== void 0) {
86
+ encodedError.push(encodeWire(error.cause, depth + 1));
87
+ }
88
+ return encodedError;
89
+ }
90
+ if (value instanceof URL) {
91
+ return [TAG, "url", value.href];
92
+ }
93
+ if (value instanceof Map) {
94
+ return [TAG, "map", [...value.entries()].map(([k, v]) => [encodeWire(k, depth + 1), encodeWire(v, depth + 1)])];
95
+ }
96
+ if (value instanceof Set) {
97
+ return [TAG, "set", [...value].map((item) => encodeWire(item, depth + 1))];
98
+ }
99
+ if (value instanceof ArrayBuffer) {
100
+ return [TAG, "bytes", toBase64(new Uint8Array(value)), "ArrayBuffer"];
101
+ }
102
+ if (ArrayBuffer.isView(value)) {
103
+ const view = value;
104
+ const ctorName = view.constructor.name;
105
+ const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
106
+ return ctorName === "Uint8Array" ? [TAG, "bytes", toBase64(bytes)] : [TAG, "bytes", toBase64(bytes), ctorName];
107
+ }
108
+ if (Array.isArray(value)) {
109
+ const encoded = value.map((item) => encodeWire(item, depth + 1));
110
+ return encoded.length > 0 && encoded[0] === TAG ? [TAG, "arr", encoded] : encoded;
111
+ }
112
+ const proto = Object.getPrototypeOf(value);
113
+ if (proto !== null && proto !== Object.prototype) {
114
+ const name = value.constructor?.name ?? "value";
115
+ throw new TypeError(
116
+ `wire-codec: cannot encode a ${name} over the Lunora wire — only plain objects, arrays, and the supported built-ins (Date, Error, URL, Map, Set, ArrayBuffer/typed arrays, bigint) round-trip`
117
+ );
118
+ }
119
+ const source = value;
120
+ const result = {};
121
+ for (const key of Object.keys(source)) {
122
+ const field = source[key];
123
+ if (field !== void 0) {
124
+ result[key] = encodeWire(field, depth + 1);
125
+ }
126
+ }
127
+ return result;
128
+ };
129
+ const decodeWire = (value, depth = 0) => {
130
+ if (depth > MAX_DEPTH) {
131
+ throw new RangeError(`wire-codec: value nesting exceeds the ${MAX_DEPTH}-level limit`);
132
+ }
133
+ if (value === null || typeof value !== "object") {
134
+ return value;
135
+ }
136
+ if (Array.isArray(value)) {
137
+ if (value[0] === TAG) {
138
+ const tag = value[1];
139
+ switch (tag) {
140
+ case "-inf": {
141
+ return -Infinity;
142
+ }
143
+ case "arr": {
144
+ return value[2].map((item) => decodeWire(item, depth + 1));
145
+ }
146
+ case "bigint": {
147
+ const raw = value[2];
148
+ if (typeof raw !== "string" || raw.length > MAX_BIGINT_DIGITS || !/^-?\d+$/.test(raw)) {
149
+ throw new RangeError(`wire-codec: invalid or over-long bigint (max ${MAX_BIGINT_DIGITS} digits)`);
150
+ }
151
+ return BigInt(raw);
152
+ }
153
+ case "date": {
154
+ return new Date(decodeWire(value[2], depth + 1));
155
+ }
156
+ case "map": {
157
+ return new Map(value[2].map(([k, v]) => [decodeWire(k, depth + 1), decodeWire(v, depth + 1)]));
158
+ }
159
+ case "set": {
160
+ return new Set(value[2].map((item) => decodeWire(item, depth + 1)));
161
+ }
162
+ case "url": {
163
+ return new URL(value[2]);
164
+ }
165
+ case "error": {
166
+ const name = value[2];
167
+ const message = value[3];
168
+ const Ctor = (Object.hasOwn(ERROR_CTORS, name) ? ERROR_CTORS[name] : void 0) ?? Error;
169
+ const error = new Ctor(message);
170
+ if (error.name !== name) {
171
+ Object.defineProperty(error, "name", { configurable: true, value: name, writable: true });
172
+ }
173
+ Object.assign(error, decodeWire(value[4], depth + 1));
174
+ if (value.length > 5) {
175
+ Object.defineProperty(error, "cause", { configurable: true, value: decodeWire(value[5], depth + 1), writable: true });
176
+ }
177
+ return error;
178
+ }
179
+ case "bytes": {
180
+ const bytes = fromBase64(value[2]);
181
+ const ctorName = value[3] ?? "Uint8Array";
182
+ if (ctorName === "ArrayBuffer") {
183
+ return bytes.buffer.byteLength === bytes.byteLength ? bytes.buffer : bytes.slice().buffer;
184
+ }
185
+ const Ctor = Object.hasOwn(TYPED_ARRAY_CTORS, ctorName) ? TYPED_ARRAY_CTORS[ctorName] : void 0;
186
+ return Ctor ? new Ctor(bytes.slice().buffer) : bytes;
187
+ }
188
+ case "inf": {
189
+ return Infinity;
190
+ }
191
+ case "nan": {
192
+ return Number.NaN;
193
+ }
194
+ case "undefined": {
195
+ return void 0;
196
+ }
197
+ default: {
198
+ return value.map((item) => decodeWire(item, depth + 1));
199
+ }
200
+ }
201
+ }
202
+ return value.map((item) => decodeWire(item, depth + 1));
203
+ }
204
+ const source = value;
205
+ const result = {};
206
+ for (const key of Object.keys(source)) {
207
+ const decoded = decodeWire(source[key], depth + 1);
208
+ if (key === UNSAFE_KEY) {
209
+ Object.defineProperty(result, key, { configurable: true, enumerable: true, value: decoded, writable: true });
210
+ } else {
211
+ result[key] = decoded;
212
+ }
213
+ }
214
+ return result;
215
+ };
216
+
217
+ const ROW_ID_FIELD = "_id";
218
+ const DELTA_FALLBACK_TABLE = "__lunora__";
219
+ const readRowId = (row) => {
220
+ if (typeof row !== "object" || row === null || Array.isArray(row)) {
221
+ return void 0;
222
+ }
223
+ const id = row[ROW_ID_FIELD];
224
+ return typeof id === "string" ? id : void 0;
225
+ };
226
+ const indexRowsById = (rows) => {
227
+ const byId = /* @__PURE__ */ new Map();
228
+ const order = [];
229
+ for (const row of rows) {
230
+ const id = readRowId(row);
231
+ if (id === void 0 || byId.has(id)) {
232
+ return void 0;
233
+ }
234
+ byId.set(id, row);
235
+ order.push(id);
236
+ }
237
+ return { byId, order };
238
+ };
239
+ const survivorsKeepOrder = (previous, next) => {
240
+ const survivingPrevious = previous.order.filter((id) => next.byId.has(id));
241
+ const survivingNext = next.order.filter((id) => previous.byId.has(id));
242
+ if (survivingPrevious.length !== survivingNext.length) {
243
+ return false;
244
+ }
245
+ return survivingPrevious.every((id, index) => survivingNext[index] === id);
246
+ };
247
+ const collectDeleteDeltas = (previous, next, deltaTable, tableJson) => {
248
+ const out = [];
249
+ for (const id of previous.order) {
250
+ if (!next.byId.has(id)) {
251
+ out.push({
252
+ delta: { key: id, op: "delete", table: deltaTable },
253
+ frame: `{"key":${JSON.stringify(id)},"op":"delete","table":${tableJson}}`
254
+ });
255
+ }
256
+ }
257
+ return out;
258
+ };
259
+ const collectUpsertDeltas = (previous, next, deltaTable, tableJson) => {
260
+ const out = [];
261
+ for (const id of next.order) {
262
+ const nextRow = next.byId.get(id);
263
+ const previousRow = previous.byId.get(id);
264
+ const nextFingerprint = JSON.stringify(encodeWire(nextRow));
265
+ const previousFingerprint = previousRow === void 0 ? void 0 : JSON.stringify(previousRow);
266
+ if (previousFingerprint === nextFingerprint) {
267
+ continue;
268
+ }
269
+ const op = previousFingerprint === void 0 ? "insert" : "update";
270
+ out.push({
271
+ delta: { key: id, op, row: nextRow, table: deltaTable },
272
+ frame: `{"key":${JSON.stringify(id)},"op":"${op}","row":${nextFingerprint},"table":${tableJson}}`
273
+ });
274
+ }
275
+ return out;
276
+ };
277
+ const subscriptionListDeltas = (previousJson, nextResult, table, frames) => {
278
+ let parsed;
279
+ try {
280
+ parsed = JSON.parse(previousJson);
281
+ } catch {
282
+ return void 0;
283
+ }
284
+ if (!Array.isArray(parsed) || !Array.isArray(nextResult)) {
285
+ return void 0;
286
+ }
287
+ const previous = indexRowsById(parsed);
288
+ const next = indexRowsById(nextResult);
289
+ if (previous === void 0 || next === void 0) {
290
+ return void 0;
291
+ }
292
+ if (!survivorsKeepOrder(previous, next)) {
293
+ return void 0;
294
+ }
295
+ const deltaTable = table === "" ? DELTA_FALLBACK_TABLE : table;
296
+ const tableJson = JSON.stringify(deltaTable);
297
+ const framed = [...collectDeleteDeltas(previous, next, deltaTable, tableJson), ...collectUpsertDeltas(previous, next, deltaTable, tableJson)];
298
+ if (framed.length > next.order.length) {
299
+ return void 0;
300
+ }
301
+ if (frames !== void 0) {
302
+ for (const { frame } of framed) {
303
+ frames.push(frame);
304
+ }
305
+ }
306
+ return framed.map(({ delta }) => delta);
307
+ };
308
+ const trySendFrame = (ws, frame) => {
309
+ try {
310
+ ws.send(frame);
311
+ return true;
312
+ } catch {
313
+ return false;
314
+ }
315
+ };
316
+ const sendDeltaFrames = (ws, subId, deltaFrames, cursorSuffix) => {
317
+ const idJson = JSON.stringify(subId);
318
+ let delivered = true;
319
+ for (const deltaBody of deltaFrames) {
320
+ if (!trySendFrame(ws, `{"type":"delta","id":${idJson},"delta":${deltaBody}${cursorSuffix}}`)) {
321
+ delivered = false;
322
+ }
323
+ }
324
+ return delivered;
325
+ };
326
+ const awaitWsDrain = async (ws) => {
327
+ let attempts = 0;
328
+ while (attempts < 100) {
329
+ attempts += 1;
330
+ const buffered = ws.bufferedAmount;
331
+ if (typeof buffered !== "number" || buffered < 1048576) {
332
+ return;
333
+ }
334
+ await new Promise((resolve) => {
335
+ setTimeout(resolve, 20);
336
+ });
337
+ }
338
+ };
339
+
340
+ export { awaitWsDrain as a, sendDeltaFrames as b, decodeWire as d, encodeWire as e, subscriptionListDeltas as s, trySendFrame as t };
@@ -0,0 +1 @@
1
+ export { a as awaitWsDrain, b as sendDeltaFrames, s as subscriptionListDeltas, t as trySendFrame } from './subscription-delivery-Z6nXHvQN.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/do",
3
- "version": "1.0.0-alpha.14",
3
+ "version": "1.0.0-alpha.15",
4
4
  "description": "Lunora Durable Objects: ShardDO (SQLite, OCC, hibernated WebSocket subscriptions) and SessionDO",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -1,124 +0,0 @@
1
- const ROW_ID_FIELD = "_id";
2
- const DELTA_FALLBACK_TABLE = "__lunora__";
3
- const readRowId = (row) => {
4
- if (typeof row !== "object" || row === null || Array.isArray(row)) {
5
- return void 0;
6
- }
7
- const id = row[ROW_ID_FIELD];
8
- return typeof id === "string" ? id : void 0;
9
- };
10
- const indexRowsById = (rows) => {
11
- const byId = /* @__PURE__ */ new Map();
12
- const order = [];
13
- for (const row of rows) {
14
- const id = readRowId(row);
15
- if (id === void 0 || byId.has(id)) {
16
- return void 0;
17
- }
18
- byId.set(id, row);
19
- order.push(id);
20
- }
21
- return { byId, order };
22
- };
23
- const survivorsKeepOrder = (previous, next) => {
24
- const survivingPrevious = previous.order.filter((id) => next.byId.has(id));
25
- const survivingNext = next.order.filter((id) => previous.byId.has(id));
26
- if (survivingPrevious.length !== survivingNext.length) {
27
- return false;
28
- }
29
- return survivingPrevious.every((id, index) => survivingNext[index] === id);
30
- };
31
- const collectDeleteDeltas = (previous, next, deltaTable, tableJson) => {
32
- const out = [];
33
- for (const id of previous.order) {
34
- if (!next.byId.has(id)) {
35
- out.push({
36
- delta: { key: id, op: "delete", table: deltaTable },
37
- frame: `{"key":${JSON.stringify(id)},"op":"delete","table":${tableJson}}`
38
- });
39
- }
40
- }
41
- return out;
42
- };
43
- const collectUpsertDeltas = (previous, next, deltaTable, tableJson) => {
44
- const out = [];
45
- for (const id of next.order) {
46
- const nextRow = next.byId.get(id);
47
- const previousRow = previous.byId.get(id);
48
- const nextFingerprint = JSON.stringify(nextRow);
49
- const previousFingerprint = previousRow === void 0 ? void 0 : JSON.stringify(previousRow);
50
- if (previousFingerprint === nextFingerprint) {
51
- continue;
52
- }
53
- const op = previousFingerprint === void 0 ? "insert" : "update";
54
- out.push({
55
- delta: { key: id, op, row: nextRow, table: deltaTable },
56
- frame: `{"key":${JSON.stringify(id)},"op":"${op}","row":${nextFingerprint},"table":${tableJson}}`
57
- });
58
- }
59
- return out;
60
- };
61
- const subscriptionListDeltas = (previousJson, nextResult, table, frames) => {
62
- let parsed;
63
- try {
64
- parsed = JSON.parse(previousJson);
65
- } catch {
66
- return void 0;
67
- }
68
- if (!Array.isArray(parsed) || !Array.isArray(nextResult)) {
69
- return void 0;
70
- }
71
- const previous = indexRowsById(parsed);
72
- const next = indexRowsById(nextResult);
73
- if (previous === void 0 || next === void 0) {
74
- return void 0;
75
- }
76
- if (!survivorsKeepOrder(previous, next)) {
77
- return void 0;
78
- }
79
- const deltaTable = table === "" ? DELTA_FALLBACK_TABLE : table;
80
- const tableJson = JSON.stringify(deltaTable);
81
- const framed = [...collectDeleteDeltas(previous, next, deltaTable, tableJson), ...collectUpsertDeltas(previous, next, deltaTable, tableJson)];
82
- if (framed.length > next.order.length) {
83
- return void 0;
84
- }
85
- if (frames !== void 0) {
86
- for (const { frame } of framed) {
87
- frames.push(frame);
88
- }
89
- }
90
- return framed.map(({ delta }) => delta);
91
- };
92
- const trySendFrame = (ws, frame) => {
93
- try {
94
- ws.send(frame);
95
- return true;
96
- } catch {
97
- return false;
98
- }
99
- };
100
- const sendDeltaFrames = (ws, subId, deltaFrames, cursorSuffix) => {
101
- const idJson = JSON.stringify(subId);
102
- let delivered = true;
103
- for (const deltaBody of deltaFrames) {
104
- if (!trySendFrame(ws, `{"type":"delta","id":${idJson},"delta":${deltaBody}${cursorSuffix}}`)) {
105
- delivered = false;
106
- }
107
- }
108
- return delivered;
109
- };
110
- const awaitWsDrain = async (ws) => {
111
- let attempts = 0;
112
- while (attempts < 100) {
113
- attempts += 1;
114
- const buffered = ws.bufferedAmount;
115
- if (typeof buffered !== "number" || buffered < 1048576) {
116
- return;
117
- }
118
- await new Promise((resolve) => {
119
- setTimeout(resolve, 20);
120
- });
121
- }
122
- };
123
-
124
- export { awaitWsDrain, sendDeltaFrames, subscriptionListDeltas, trySendFrame };