@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 +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.mjs +7 -7
- package/dist/packem_shared/{ROOT_DO_SIZE_WARN_BYTES-DXVOGLBS.mjs → ROOT_DO_SIZE_WARN_BYTES-CEVhBec2.mjs} +114 -13
- package/dist/packem_shared/{ReactiveCache-1hDydFyv.mjs → ReactiveCache-BYlSGY0N.mjs} +1 -1
- package/dist/packem_shared/{diffExternalSource-Br2GYVZB.mjs → diffExternalSource-Cx9HUPJj.mjs} +1 -1
- package/dist/packem_shared/{isSourceDue-D_wTv871.mjs → isSourceDue-BMTJv65e.mjs} +1 -1
- package/dist/packem_shared/{materializeExternalRows-CcsfmiDe.mjs → materializeExternalRows-uKBx9H0s.mjs} +2 -2
- package/dist/packem_shared/{stableStringify-CyHKJXre.mjs → stableStringify-MydiuScU.mjs} +10 -0
- package/dist/packem_shared/subscription-delivery-Z6nXHvQN.mjs +340 -0
- package/dist/packem_shared/subscriptionListDeltas-CXwfoEE_.mjs +1 -0
- package/package.json +1 -1
- package/dist/packem_shared/subscriptionListDeltas-DoB136JT.mjs +0 -124
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-
|
|
11
|
-
export { materializeExternalRows, readExternalSourceBaseline, runExternalSourceTick } from './packem_shared/materializeExternalRows-
|
|
12
|
-
export { isSourceDue, liftSourceId, pullExternalSourceTick } from './packem_shared/isSourceDue-
|
|
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-
|
|
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-
|
|
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-
|
|
40
|
-
export { subscriptionListDeltas } from './packem_shared/
|
|
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-
|
|
12
|
-
import { stableStringify } from './stableStringify-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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,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-
|
|
4
|
-
import { stableStringify } from './stableStringify-
|
|
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,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 };
|