@lunora/do 1.0.0-alpha.16 → 1.0.0-alpha.18
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 +11 -4
- package/dist/index.d.ts +11 -4
- package/dist/index.mjs +7 -7
- package/dist/packem_shared/{ADMIN_FUNCTIONS-fMl1vyt1.mjs → ADMIN_FUNCTIONS-BvGiv2u6.mjs} +2 -1
- package/dist/packem_shared/{NotUniqueError-BXn2j5vu.mjs → NotUniqueError-Do5h_jiW.mjs} +2 -2
- package/dist/packem_shared/{ROOT_DO_SIZE_WARN_BYTES-CtTkRXH0.mjs → ROOT_DO_SIZE_WARN_BYTES-DHNOI0RZ.mjs} +126 -38
- package/dist/packem_shared/{compileWhereSql-CXrhFA3G.mjs → compileWhereSql-MW_Lk8nJ.mjs} +3 -2
- package/dist/packem_shared/{ctx-db-shapes-BQapRFjB.mjs → ctx-db-shapes-0RaIOy7J.mjs} +1 -1
- package/dist/packem_shared/{isSourceDue-BMTJv65e.mjs → isSourceDue-9mJRJ9Ld.mjs} +1 -1
- package/dist/packem_shared/{materializeExternalRows-uKBx9H0s.mjs → materializeExternalRows-Bj6EXy50.mjs} +1 -1
- package/dist/packem_shared/{serveRelationFanout-EZLD9-O3.mjs → serveRelationFanout-BuSmWawx.mjs} +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -5846,8 +5846,12 @@ declare abstract class ShardDO {
|
|
|
5846
5846
|
* subscriber counts are folded live from each socket's attachment via
|
|
5847
5847
|
* {@link summarizeFanoutTopics}; the running per-path cost counters are the
|
|
5848
5848
|
* in-memory {@link ShardDO.fanout} tallies, sharing `metrics.sinceMs` as the
|
|
5849
|
-
* "since this instance woke" epoch.
|
|
5850
|
-
*
|
|
5849
|
+
* "since this instance woke" epoch. Touches no SQLite and mutates no socket
|
|
5850
|
+
* state; it does call `relay.relayCount()`, which advances the promotion
|
|
5851
|
+
* latch — safe here because that transition is a pure, monotonic function of
|
|
5852
|
+
* the live socket count (DOs are single-threaded), so a metrics poll only ever
|
|
5853
|
+
* drives the latch to the same state the routing path would compute for the
|
|
5854
|
+
* same count, never a divergent one.
|
|
5851
5855
|
*/
|
|
5852
5856
|
private collectFanoutMetrics;
|
|
5853
5857
|
/** Resolve a `getAuditLog` admin read, parsing the optional `limit`/`sinceSeq` cursor args and ensuring the reserved table first. */
|
|
@@ -6593,8 +6597,11 @@ interface WhereSqlStrategy {
|
|
|
6593
6597
|
fieldRef: FieldRefSql;
|
|
6594
6598
|
/**
|
|
6595
6599
|
* Dialect `contains` rendering given the field reference and the (already
|
|
6596
|
-
* bound) search term. Absent ⇒ the portable
|
|
6597
|
-
* concat form (SQLite/Postgres); MySQL
|
|
6600
|
+
* bound, already wildcard-escaped) search term. Absent ⇒ the portable
|
|
6601
|
+
* `… LIKE '%' || term || '%' ESCAPE '\'` concat form (SQLite/Postgres); MySQL
|
|
6602
|
+
* supplies a `CONCAT(...)` variant. The term is escaped by
|
|
6603
|
+
* {@link compileContains}, so an implementation MUST pair it with
|
|
6604
|
+
* `ESCAPE '\'` for the literal-match to hold.
|
|
6598
6605
|
*/
|
|
6599
6606
|
likeContains?: (reference: SQL, term: SQL) => SQL;
|
|
6600
6607
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -5846,8 +5846,12 @@ declare abstract class ShardDO {
|
|
|
5846
5846
|
* subscriber counts are folded live from each socket's attachment via
|
|
5847
5847
|
* {@link summarizeFanoutTopics}; the running per-path cost counters are the
|
|
5848
5848
|
* in-memory {@link ShardDO.fanout} tallies, sharing `metrics.sinceMs` as the
|
|
5849
|
-
* "since this instance woke" epoch.
|
|
5850
|
-
*
|
|
5849
|
+
* "since this instance woke" epoch. Touches no SQLite and mutates no socket
|
|
5850
|
+
* state; it does call `relay.relayCount()`, which advances the promotion
|
|
5851
|
+
* latch — safe here because that transition is a pure, monotonic function of
|
|
5852
|
+
* the live socket count (DOs are single-threaded), so a metrics poll only ever
|
|
5853
|
+
* drives the latch to the same state the routing path would compute for the
|
|
5854
|
+
* same count, never a divergent one.
|
|
5851
5855
|
*/
|
|
5852
5856
|
private collectFanoutMetrics;
|
|
5853
5857
|
/** Resolve a `getAuditLog` admin read, parsing the optional `limit`/`sinceSeq` cursor args and ensuring the reserved table first. */
|
|
@@ -6593,8 +6597,11 @@ interface WhereSqlStrategy {
|
|
|
6593
6597
|
fieldRef: FieldRefSql;
|
|
6594
6598
|
/**
|
|
6595
6599
|
* Dialect `contains` rendering given the field reference and the (already
|
|
6596
|
-
* bound) search term. Absent ⇒ the portable
|
|
6597
|
-
* concat form (SQLite/Postgres); MySQL
|
|
6600
|
+
* bound, already wildcard-escaped) search term. Absent ⇒ the portable
|
|
6601
|
+
* `… LIKE '%' || term || '%' ESCAPE '\'` concat form (SQLite/Postgres); MySQL
|
|
6602
|
+
* supplies a `CONCAT(...)` variant. The term is escaped by
|
|
6603
|
+
* {@link compileContains}, so an implementation MUST pair it with
|
|
6604
|
+
* `ESCAPE '\'` for the literal-match to hold.
|
|
6598
6605
|
*/
|
|
6599
6606
|
likeContains?: (reference: SQL, term: SQL) => SQL;
|
|
6600
6607
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -3,15 +3,15 @@ export { AGGREGATE_SQL_FUNCTION, aggregateSqlFunction, matchesStaticWhere, norma
|
|
|
3
3
|
export { aggregateTableName, coerceAggregateNumber, encodeAggregateKey, foldAggregateTally, readAggregateValue } from './packem_shared/aggregateTableName-CxNqY1Sl.mjs';
|
|
4
4
|
export { CountRlsUnsupportedError, mergeWhere, planAggregateLookup, selectIndexForAggregate, selectIndexForCount, selectIndexForGroupBy } from './packem_shared/CountRlsUnsupportedError-28ZvvwKS.mjs';
|
|
5
5
|
export { AUTH_METRICS_BUCKETS_TABLE, AUTH_METRICS_BUCKET_MS, AUTH_METRICS_BUCKET_RETENTION, AUTH_METRICS_TABLE, ensureAuthMetricsTables, readAuthMetrics, recordAuthEvent } from './packem_shared/AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs';
|
|
6
|
-
export { NotUniqueError, assertValidClientId, createShardCtxDb, normalizeIdStructurally } from './packem_shared/NotUniqueError-
|
|
6
|
+
export { NotUniqueError, assertValidClientId, createShardCtxDb, normalizeIdStructurally } from './packem_shared/NotUniqueError-Do5h_jiW.mjs';
|
|
7
7
|
export { DATA_MIGRATION_STATE_TABLE, readMigrationStatus, runDataMigration } from './packem_shared/DATA_MIGRATION_STATE_TABLE-DfPxn8I0.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
10
|
export { diffExternalSource } from './packem_shared/diffExternalSource-Cx9HUPJj.mjs';
|
|
11
|
-
export { materializeExternalRows, readExternalSourceBaseline, runExternalSourceTick } from './packem_shared/materializeExternalRows-
|
|
12
|
-
export { isSourceDue, liftSourceId, pullExternalSourceTick } from './packem_shared/isSourceDue-
|
|
11
|
+
export { materializeExternalRows, readExternalSourceBaseline, runExternalSourceTick } from './packem_shared/materializeExternalRows-Bj6EXy50.mjs';
|
|
12
|
+
export { isSourceDue, liftSourceId, pullExternalSourceTick } from './packem_shared/isSourceDue-9mJRJ9Ld.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
|
-
export { ADMIN_FUNCTIONS, ADMIN_FUNCTION_PREFIX, FLAGS_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, facetColumn, listTables, readTablePage, selectMatchingIds } from './packem_shared/ADMIN_FUNCTIONS-
|
|
14
|
+
export { ADMIN_FUNCTIONS, ADMIN_FUNCTION_PREFIX, FLAGS_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, facetColumn, listTables, readTablePage, selectMatchingIds } from './packem_shared/ADMIN_FUNCTIONS-BvGiv2u6.mjs';
|
|
15
15
|
export { LogBuffer } from './packem_shared/LogBuffer-B_Ezju_N.mjs';
|
|
16
16
|
export { MAIL_RETENTION, MAIL_TABLE, clearCapturedMail, ensureMailTable, readCapturedMail, recordCapturedMail } from './packem_shared/MAIL_RETENTION-CPpgl-dX.mjs';
|
|
17
17
|
export { default as NotFoundError } from './packem_shared/NotFoundError-CMuMZt81.mjs';
|
|
@@ -19,20 +19,20 @@ export { armRestore, readBookmark } from './packem_shared/armRestore-BJk53Ro8.mj
|
|
|
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
21
|
export { ReactiveCache, reactiveCacheKey } from './packem_shared/ReactiveCache-BYlSGY0N.mjs';
|
|
22
|
-
export { serveRelationFanout } from './packem_shared/serveRelationFanout-
|
|
22
|
+
export { serveRelationFanout } from './packem_shared/serveRelationFanout-BuSmWawx.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';
|
|
25
25
|
export { RLS_UNWRAP_SYMBOL, RlsRequiredError, guardWriter } from './packem_shared/RLS_UNWRAP_SYMBOL-EtGQdC9d.mjs';
|
|
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-DHNOI0RZ.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';
|
|
33
33
|
export { ConflictError } from './packem_shared/ConflictError-C0STs6bU.mjs';
|
|
34
34
|
export { hasTrigger, runTriggers } from './packem_shared/hasTrigger-5N6_Fx0A.mjs';
|
|
35
|
-
export { compileWhereSql } from './packem_shared/compileWhereSql-
|
|
35
|
+
export { compileWhereSql } from './packem_shared/compileWhereSql-MW_Lk8nJ.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';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
2
|
+
|
|
1
3
|
const ADMIN_FUNCTION_PREFIX = "__lunora_admin__:";
|
|
2
4
|
const RELATION_FUNCTION_PREFIX = "__lunora_relation__:";
|
|
3
5
|
const FLAGS_FUNCTION_PREFIX = "__lunora_flags__:";
|
|
@@ -95,7 +97,6 @@ const expandDocumentRows = (columns, rows) => {
|
|
|
95
97
|
};
|
|
96
98
|
const escapeLike = (value) => value.replaceAll(/[\\%_]/g, (character) => `\\${character}`);
|
|
97
99
|
const isInternalTable = (name) => name.startsWith("sqlite_") || name.startsWith("_cf_") || name.startsWith("__miniflare") || name.startsWith("__lunora") || name.includes("__fts_");
|
|
98
|
-
const quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
99
100
|
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
100
101
|
const countRows = (sql, quotedTable) => {
|
|
101
102
|
const row = sql.exec(`SELECT COUNT(*) AS c FROM ${quotedTable}`).one();
|
|
@@ -19,11 +19,11 @@ import { guardWriter } from './RLS_UNWRAP_SYMBOL-EtGQdC9d.mjs';
|
|
|
19
19
|
import { createSystemReader } from './createSystemReader-8CzSZP9V.mjs';
|
|
20
20
|
import { ConflictError } from './ConflictError-C0STs6bU.mjs';
|
|
21
21
|
import { runTriggers } from './hasTrigger-5N6_Fx0A.mjs';
|
|
22
|
-
import { compileWhereSql } from './compileWhereSql-
|
|
22
|
+
import { compileWhereSql } from './compileWhereSql-MW_Lk8nJ.mjs';
|
|
23
23
|
export { backfillAggregateIndexes, backfillRankIndexes } from './backfillAggregateIndexes-BZsOqDXP.mjs';
|
|
24
24
|
export { C as CLIENT_WATERMARK_TABLE, G as GLOBAL_SHAPE_SNAPSHOT_TABLE, I as IDEMPOTENCY_TABLE, c as advanceClientWatermark, d as deleteGlobalShapeSnapshot, e as deleteGlobalShapeSnapshotsForConnection, m as migrateClientWatermark, b as migrateGlobalShapeSnapshot, r as readClientWatermark, f as readGlobalShapeSnapshot, g as readIdempotent, t as trimIdempotent, w as writeGlobalShapeSnapshot, h as writeIdempotent } from './ctx-db-idempotency-BdcNpvY4.mjs';
|
|
25
25
|
export { runShardMigrations } from './runShardMigrations-nIwoQeOK.mjs';
|
|
26
|
-
export { a as selectShapeMemberIds, s as selectShapeRows } from './ctx-db-shapes-
|
|
26
|
+
export { a as selectShapeMemberIds, s as selectShapeRows } from './ctx-db-shapes-0RaIOy7J.mjs';
|
|
27
27
|
|
|
28
28
|
const rankIndexFieldsUnchanged = (index, previous, next) => {
|
|
29
29
|
const fields = [...index.partitionBy ?? [], ...index.sortBy.map((key) => key.field), ...index.where ? Object.keys(index.where) : []];
|
|
@@ -5,7 +5,7 @@ import { recordAuthEvent, readAuthMetrics } from './AUTH_METRICS_BUCKETS_TABLE-C
|
|
|
5
5
|
import { DATA_MIGRATION_STATE_TABLE, readMigrationStatus } from './DATA_MIGRATION_STATE_TABLE-DfPxn8I0.mjs';
|
|
6
6
|
import { SCAN_DEP, createDependencyTracker, tableFromDepKey } from './SCAN_DEP-DLJF8dsj.mjs';
|
|
7
7
|
import { readFunctionMetricsTotals, readFunctionMetricIndexHits, recordFunctionMetric, mergeScanAttribution, readFunctionMetrics, readFunctionMetricBuckets } from './FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs';
|
|
8
|
-
import { createFanoutCounters, ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, selectMatchingIds, ADMIN_FUNCTIONS, findStorageReferences, listTables, summarizeSubscriptions, summarizeFanoutTopics, readTablePage, facetColumn, FLAGS_FUNCTION_PREFIX, recordFanoutPass, MAX_PAGE_SIZE } from './ADMIN_FUNCTIONS-
|
|
8
|
+
import { createFanoutCounters, ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, selectMatchingIds, ADMIN_FUNCTIONS, findStorageReferences, listTables, summarizeSubscriptions, summarizeFanoutTopics, readTablePage, facetColumn, FLAGS_FUNCTION_PREFIX, recordFanoutPass, MAX_PAGE_SIZE } from './ADMIN_FUNCTIONS-BvGiv2u6.mjs';
|
|
9
9
|
import { LogBuffer } from './LogBuffer-B_Ezju_N.mjs';
|
|
10
10
|
import { recordCapturedMail, clearCapturedMail, readCapturedMail, MAIL_TABLE } from './MAIL_RETENTION-CPpgl-dX.mjs';
|
|
11
11
|
import { readBookmark, armRestore } from './armRestore-BJk53Ro8.mjs';
|
|
@@ -17,7 +17,7 @@ import { runReadonlySql } from './MAX_SQL_ROWS-dDcFE1YZ.mjs';
|
|
|
17
17
|
import { ConflictError } from './ConflictError-C0STs6bU.mjs';
|
|
18
18
|
import { e as deleteGlobalShapeSnapshotsForConnection, g as readIdempotent, h as writeIdempotent, t as trimIdempotent, r as readClientWatermark, m as migrateClientWatermark, c as advanceClientWatermark, d as deleteGlobalShapeSnapshot, f as readGlobalShapeSnapshot, w as writeGlobalShapeSnapshot } from './ctx-db-idempotency-BdcNpvY4.mjs';
|
|
19
19
|
import { CDC_LOG_TABLE, readCdcChanges, readCdcCursor, readCdcEpoch, minCdcSeq, bumpCdcEpoch } from './CDC_LOG_TABLE-DSycmnDf.mjs';
|
|
20
|
-
import { a as selectShapeMemberIds, s as selectShapeRows } from './ctx-db-shapes-
|
|
20
|
+
import { a as selectShapeMemberIds, s as selectShapeRows } from './ctx-db-shapes-0RaIOy7J.mjs';
|
|
21
21
|
|
|
22
22
|
const MAX_BATCH_ENTRIES = 500;
|
|
23
23
|
|
|
@@ -196,7 +196,22 @@ const parseRelayName = (name) => {
|
|
|
196
196
|
};
|
|
197
197
|
|
|
198
198
|
const shapeRoutingKey = (name, args) => stableStringify({ args: args ?? {}, name });
|
|
199
|
-
const DEFAULT_PROMOTION_THRESHOLDS = { tUp: 8e3 };
|
|
199
|
+
const DEFAULT_PROMOTION_THRESHOLDS = { tDown: 4e3, tUp: 8e3 };
|
|
200
|
+
const nextPromotionState = (current, subscribers, thresholds = DEFAULT_PROMOTION_THRESHOLDS) => {
|
|
201
|
+
if (thresholds.tDown >= thresholds.tUp) {
|
|
202
|
+
throw new Error(`invalid promotion thresholds: tDown (${String(thresholds.tDown)}) must be < tUp (${String(thresholds.tUp)})`);
|
|
203
|
+
}
|
|
204
|
+
if (current === "owned") {
|
|
205
|
+
return subscribers >= thresholds.tUp ? "promoted" : "owned";
|
|
206
|
+
}
|
|
207
|
+
return subscribers < thresholds.tDown ? "owned" : "promoted";
|
|
208
|
+
};
|
|
209
|
+
const clampPromotionThresholds = (tUp, tDownRaw) => {
|
|
210
|
+
if (tDownRaw < tUp) {
|
|
211
|
+
return { tDown: tDownRaw, tUp };
|
|
212
|
+
}
|
|
213
|
+
return { tDown: Math.min(Math.max(1, Math.floor(tUp / 2)), tUp - 1), tUp };
|
|
214
|
+
};
|
|
200
215
|
|
|
201
216
|
const projectColumns = (document_, columns) => {
|
|
202
217
|
if (!columns) {
|
|
@@ -232,11 +247,12 @@ const diffGlobalMembership = (rows, previous, options) => {
|
|
|
232
247
|
}
|
|
233
248
|
return { next, rowsPatch };
|
|
234
249
|
};
|
|
235
|
-
const
|
|
250
|
+
const encodeRowsPatch = (rowsPatch) => rowsPatch.map((op) => op.value === void 0 ? op : { ...op, value: encodeWire(op.value) });
|
|
251
|
+
const buildPokeFrames = (parts, meta, options = {}) => {
|
|
236
252
|
const { baseCheckpoint, checkpoint, epoch, lastMutationId, pokeId } = meta;
|
|
237
253
|
const frames = [JSON.stringify({ baseCheckpoint, epoch, pokeId, type: "pokeStart" })];
|
|
238
254
|
for (const part of parts) {
|
|
239
|
-
const rowsPatch =
|
|
255
|
+
const rowsPatch = options.preEncoded ? part.rowsPatch : encodeRowsPatch(part.rowsPatch);
|
|
240
256
|
frames.push(
|
|
241
257
|
JSON.stringify({
|
|
242
258
|
pokeId,
|
|
@@ -253,6 +269,30 @@ const buildPokeFrames = (parts, meta) => {
|
|
|
253
269
|
|
|
254
270
|
const DEFAULT_RELAY_FAN = 2;
|
|
255
271
|
const DEFAULT_MAX_RELAYS = 8;
|
|
272
|
+
const RELAY_SECRET_KEY = "LUNORA_RELAY_SECRET";
|
|
273
|
+
const RELAY_SIGNATURE_HEADER = "x-lunora-relay-sig";
|
|
274
|
+
const relaySecretOf = (env) => {
|
|
275
|
+
const value = env?.[RELAY_SECRET_KEY];
|
|
276
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
277
|
+
};
|
|
278
|
+
const constantTimeEqual$1 = (a, b) => {
|
|
279
|
+
if (a.length !== b.length) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
let mismatch = 0;
|
|
283
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
284
|
+
const charA = a.charCodeAt(index);
|
|
285
|
+
const charB = b.charCodeAt(index);
|
|
286
|
+
mismatch |= charA ^ charB;
|
|
287
|
+
}
|
|
288
|
+
return mismatch === 0;
|
|
289
|
+
};
|
|
290
|
+
const signRelayBody = async (secret, body) => {
|
|
291
|
+
const encoder = new TextEncoder();
|
|
292
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { hash: "SHA-256", name: "HMAC" }, false, ["sign"]);
|
|
293
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
|
294
|
+
return [...new Uint8Array(signature)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
295
|
+
};
|
|
256
296
|
const envPositiveInt = (env, key, fallback) => {
|
|
257
297
|
const raw = env?.[key];
|
|
258
298
|
let parsed = Number.NaN;
|
|
@@ -289,9 +329,23 @@ class RelayLink {
|
|
|
289
329
|
* to `relay.ts` without a case here is a COMPILE error, not a runtime mis-route.
|
|
290
330
|
*/
|
|
291
331
|
async handleControl(request) {
|
|
332
|
+
let raw;
|
|
333
|
+
try {
|
|
334
|
+
raw = await request.text();
|
|
335
|
+
} catch {
|
|
336
|
+
return new Response("bad request", { status: 400 });
|
|
337
|
+
}
|
|
338
|
+
const secret = relaySecretOf(this.host.env());
|
|
339
|
+
if (secret !== void 0) {
|
|
340
|
+
const supplied = request.headers.get(RELAY_SIGNATURE_HEADER);
|
|
341
|
+
const expected = await signRelayBody(secret, raw);
|
|
342
|
+
if (supplied === null || !constantTimeEqual$1(supplied, expected)) {
|
|
343
|
+
return new Response("forbidden", { status: 403 });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
292
346
|
let message;
|
|
293
347
|
try {
|
|
294
|
-
message =
|
|
348
|
+
message = JSON.parse(raw);
|
|
295
349
|
} catch {
|
|
296
350
|
return new Response("bad request", { status: 400 });
|
|
297
351
|
}
|
|
@@ -356,10 +410,16 @@ class RelayLink {
|
|
|
356
410
|
return void 0;
|
|
357
411
|
}
|
|
358
412
|
const stub = typeof namespace.getByName === "function" ? namespace.getByName(targetName) : namespace.get(namespace.idFromName(targetName));
|
|
413
|
+
const body = JSON.stringify(message);
|
|
414
|
+
const headers = { "content-type": "application/json", "x-lunora-shard-binding": this.host.shardBinding() ?? "" };
|
|
415
|
+
const secret = relaySecretOf(this.host.env());
|
|
416
|
+
if (secret !== void 0) {
|
|
417
|
+
headers[RELAY_SIGNATURE_HEADER] = await signRelayBody(secret, body);
|
|
418
|
+
}
|
|
359
419
|
try {
|
|
360
420
|
return await stub.fetch("https://relay.internal/_lunora/relay", {
|
|
361
|
-
body
|
|
362
|
-
headers
|
|
421
|
+
body,
|
|
422
|
+
headers,
|
|
363
423
|
method: "POST"
|
|
364
424
|
});
|
|
365
425
|
} catch {
|
|
@@ -376,6 +436,8 @@ class OwnerRelay extends RelayLink {
|
|
|
376
436
|
relayShapeRegistry = /* @__PURE__ */ new Map();
|
|
377
437
|
/** NON-uniform (identity-scoped) relay shapes, one entry per relay socket, keyed `relayIndex:connectionId:subId`. Each is served live by a per-socket proxy poke. */
|
|
378
438
|
relayShapeProxies = /* @__PURE__ */ new Map();
|
|
439
|
+
/** The owner's current promotion state (plan 075 Phase 4), carried across `relayCount()` calls so hysteresis has memory — a shard hovering near the threshold can't flap. */
|
|
440
|
+
promotionState = "owned";
|
|
379
441
|
constructor(host, ownerKey) {
|
|
380
442
|
super(host, { ownerKey });
|
|
381
443
|
}
|
|
@@ -406,13 +468,22 @@ class OwnerRelay extends RelayLink {
|
|
|
406
468
|
}
|
|
407
469
|
/**
|
|
408
470
|
* How many relays the runtime should spread new connections across for this shard
|
|
409
|
-
* (plan 075 Phase 2). `0` keeps every connection on
|
|
410
|
-
* once its live socket count
|
|
411
|
-
* `
|
|
471
|
+
* (plan 075 Phase 2, hysteresis added in Phase 4). `0` keeps every connection on
|
|
472
|
+
* the owner. The owner promotes once its live socket count reaches
|
|
473
|
+
* `LUNORA_RELAY_THRESHOLD` (`tUp`), fanning to a fixed `LUNORA_RELAY_FAN` (capped
|
|
474
|
+
* by `LUNORA_MAX_RELAYS`, the cost ceiling), and only collapses back to
|
|
475
|
+
* owner-served once subscribers drain below `LUNORA_RELAY_COLLAPSE_THRESHOLD`
|
|
476
|
+
* (`tDown`) — the band between the two holds the current state so a shard
|
|
477
|
+
* hovering near the threshold can't flap. {@link clampPromotionThresholds}
|
|
478
|
+
* guarantees a valid `tDown < tUp` band even under a misconfigured collapse
|
|
479
|
+
* threshold. Advances the promotion latch, so it is not a pure read.
|
|
412
480
|
*/
|
|
413
481
|
relayCount() {
|
|
414
|
-
const
|
|
415
|
-
|
|
482
|
+
const subscribers = this.host.getWebSockets().length;
|
|
483
|
+
const tUp = envPositiveInt(this.host.env(), "LUNORA_RELAY_THRESHOLD", DEFAULT_PROMOTION_THRESHOLDS.tUp);
|
|
484
|
+
const tDownRaw = envPositiveInt(this.host.env(), "LUNORA_RELAY_COLLAPSE_THRESHOLD", DEFAULT_PROMOTION_THRESHOLDS.tDown);
|
|
485
|
+
this.promotionState = nextPromotionState(this.promotionState, subscribers, clampPromotionThresholds(tUp, tDownRaw));
|
|
486
|
+
if (this.promotionState === "owned") {
|
|
416
487
|
return 0;
|
|
417
488
|
}
|
|
418
489
|
const maxRelays = envPositiveInt(this.host.env(), "LUNORA_MAX_RELAYS", DEFAULT_MAX_RELAYS);
|
|
@@ -497,7 +568,10 @@ class OwnerRelay extends RelayLink {
|
|
|
497
568
|
epoch,
|
|
498
569
|
fromCursor,
|
|
499
570
|
name: entry.name,
|
|
500
|
-
|
|
571
|
+
// Wire-encode before the poke crosses the owner→relay `JSON.stringify`
|
|
572
|
+
// hop (`requestRelayMessage`); the relay re-frames it with
|
|
573
|
+
// `preEncoded` so a `bytes`/`bigint` shape column isn't dropped/truncated.
|
|
574
|
+
rowsPatch: encodeRowsPatch(rowsPatch),
|
|
501
575
|
type: "relay_shape_poke"
|
|
502
576
|
};
|
|
503
577
|
for (const index of relays) {
|
|
@@ -541,7 +615,9 @@ class OwnerRelay extends RelayLink {
|
|
|
541
615
|
epoch,
|
|
542
616
|
fromCursor,
|
|
543
617
|
name: entry.name,
|
|
544
|
-
|
|
618
|
+
// Wire-encode before the owner→relay `JSON.stringify` hop (see the
|
|
619
|
+
// cohort-multicast path); the relay re-frames it with `preEncoded`.
|
|
620
|
+
rowsPatch: encodeRowsPatch(rowsPatch),
|
|
545
621
|
targetConnectionId: entry.connectionId,
|
|
546
622
|
type: "relay_shape_poke"
|
|
547
623
|
};
|
|
@@ -647,8 +723,9 @@ class OwnerRelay extends RelayLink {
|
|
|
647
723
|
* sound against the cross-identity row-leak (review). Resolves under the anonymous
|
|
648
724
|
* multicast identity (the base) plus two `Proxy`-backed identities that return a
|
|
649
725
|
* distinct value for ANY accessed claim, requires all to agree on table + where +
|
|
650
|
-
* columns, rejects any table with an RLS read policy or
|
|
651
|
-
*
|
|
726
|
+
* columns, rejects any table with an RLS read policy or ANY masked column
|
|
727
|
+
* defined (even if unprojected — see {@link OwnerRelay.tableHasAnyMask}), and
|
|
728
|
+
* fails closed if the claims are enumerated (a wholesale copy the proxy can't
|
|
652
729
|
* differentiate).
|
|
653
730
|
*/
|
|
654
731
|
probeShapeRelayUniform(name, args) {
|
|
@@ -664,7 +741,7 @@ class OwnerRelay extends RelayLink {
|
|
|
664
741
|
if (this.host.rlsMetadata().policies.some((policy) => policy.on === "read" && policy.table === base.table)) {
|
|
665
742
|
return false;
|
|
666
743
|
}
|
|
667
|
-
if (this.
|
|
744
|
+
if (this.tableHasAnyMask(base.table)) {
|
|
668
745
|
return false;
|
|
669
746
|
}
|
|
670
747
|
const baseWhere = stableStringify(base.effectiveWhere);
|
|
@@ -702,17 +779,18 @@ class OwnerRelay extends RelayLink {
|
|
|
702
779
|
});
|
|
703
780
|
return matches && !enumerated;
|
|
704
781
|
}
|
|
705
|
-
/**
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
782
|
+
/**
|
|
783
|
+
* Whether `table` has ANY masked column defined (L7). Deliberately conservative:
|
|
784
|
+
* we disqualify a shape from relay-multicast if its table declares any mask at
|
|
785
|
+
* all, even when the current query doesn't project the masked column. A masked
|
|
786
|
+
* value is identity-dependent, and keying uniformity off the *projected* set
|
|
787
|
+
* meant a later column addition (or a `select` change) could silently widen a
|
|
788
|
+
* cohort to include an identity-dependent value. Refusing on any table-level
|
|
789
|
+
* mask removes that footgun; the cost is a few extra shapes falling back to the
|
|
790
|
+
* per-identity path, which is always correct.
|
|
791
|
+
*/
|
|
792
|
+
tableHasAnyMask(table) {
|
|
793
|
+
return this.host.maskMetadata().columns.some((entry) => entry.table === table);
|
|
716
794
|
}
|
|
717
795
|
}
|
|
718
796
|
class RelayMember extends RelayLink {
|
|
@@ -861,13 +939,19 @@ class RelayMember extends RelayLink {
|
|
|
861
939
|
if (memo?.cursor !== poke.fromCursor || memo.epoch !== poke.epoch || shapeRoutingKey(sub.name, sub.args) !== routingKey) {
|
|
862
940
|
continue;
|
|
863
941
|
}
|
|
864
|
-
const frames = buildPokeFrames(
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
942
|
+
const frames = buildPokeFrames(
|
|
943
|
+
[{ rowsPatch: poke.rowsPatch, shapeId: subId }],
|
|
944
|
+
{
|
|
945
|
+
baseCheckpoint: void 0,
|
|
946
|
+
checkpoint: poke.checkpoint,
|
|
947
|
+
epoch: poke.epoch,
|
|
948
|
+
lastMutationId: void 0,
|
|
949
|
+
pokeId: this.host.nextPokeId()
|
|
950
|
+
},
|
|
951
|
+
// `poke.rowsPatch` was wire-encoded by the owner before it crossed
|
|
952
|
+
// the hub — don't double-encode it here.
|
|
953
|
+
{ preEncoded: true }
|
|
954
|
+
);
|
|
871
955
|
for (const frame of frames) {
|
|
872
956
|
trySendFrame(ws, frame);
|
|
873
957
|
}
|
|
@@ -4651,8 +4735,12 @@ class ShardDO {
|
|
|
4651
4735
|
* subscriber counts are folded live from each socket's attachment via
|
|
4652
4736
|
* {@link summarizeFanoutTopics}; the running per-path cost counters are the
|
|
4653
4737
|
* in-memory {@link ShardDO.fanout} tallies, sharing `metrics.sinceMs` as the
|
|
4654
|
-
* "since this instance woke" epoch.
|
|
4655
|
-
*
|
|
4738
|
+
* "since this instance woke" epoch. Touches no SQLite and mutates no socket
|
|
4739
|
+
* state; it does call `relay.relayCount()`, which advances the promotion
|
|
4740
|
+
* latch — safe here because that transition is a pure, monotonic function of
|
|
4741
|
+
* the live socket count (DOs are single-threaded), so a metrics poll only ever
|
|
4742
|
+
* drives the latch to the same state the routing path would compute for the
|
|
4743
|
+
* same count, never a divergent one.
|
|
4656
4744
|
*/
|
|
4657
4745
|
collectFanoutMetrics() {
|
|
4658
4746
|
const summary = summarizeFanoutTopics(this.state.getWebSockets().map((ws) => this.readAttachment(ws)));
|
|
@@ -11,9 +11,10 @@ const isOperatorObject = (value) => {
|
|
|
11
11
|
const keys = Object.keys(value);
|
|
12
12
|
return keys.length > 0 && keys.every((key) => OPERATOR_KEY_SET.has(key));
|
|
13
13
|
};
|
|
14
|
+
const escapeLikeTerm = (value) => typeof value === "string" ? value.replaceAll(/[\\%_]/g, (character) => `\\${character}`) : value;
|
|
14
15
|
const compileContains = (reference, value, strategy) => {
|
|
15
|
-
const term = sql`${strategy.serialize(value)}`;
|
|
16
|
-
return strategy.likeContains ? strategy.likeContains(reference, term) : sql`${reference} LIKE '%' || ${term} || '%'`;
|
|
16
|
+
const term = sql`${strategy.serialize(escapeLikeTerm(value))}`;
|
|
17
|
+
return strategy.likeContains ? strategy.likeContains(reference, term) : sql`${reference} LIKE '%' || ${term} || '%' ESCAPE '\\'`;
|
|
17
18
|
};
|
|
18
19
|
const compileComparator = (reference, operator, comparator, value, strategy) => {
|
|
19
20
|
if (value === null) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { sql } from 'drizzle-orm';
|
|
2
2
|
import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
|
|
3
3
|
import { D as DOC_COLUMN, r as rowToDocument, j as jsonPathSql } from './do-sql-BCHCWtrD.mjs';
|
|
4
|
-
import { compileWhereSql } from './compileWhereSql-
|
|
4
|
+
import { compileWhereSql } from './compileWhereSql-MW_Lk8nJ.mjs';
|
|
5
5
|
import { s as serializeSqlValue } from './serialize-sql-BlRUoiQe.mjs';
|
|
6
6
|
|
|
7
7
|
const shapeWhereStrategy = { fieldRef: jsonPathSql, serialize: serializeSqlValue };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { applyCdcChanges } from './CDC_LOG_TABLE-DSycmnDf.mjs';
|
|
2
|
-
import { s as selectShapeRows } from './ctx-db-shapes-
|
|
2
|
+
import { s as selectShapeRows } from './ctx-db-shapes-0RaIOy7J.mjs';
|
|
3
3
|
import { diffExternalSource, projectExternalSourceRow } from './diffExternalSource-Cx9HUPJj.mjs';
|
|
4
4
|
import { stableStringify } from './stableStringify-MydiuScU.mjs';
|
|
5
5
|
|
package/dist/packem_shared/{serveRelationFanout-EZLD9-O3.mjs → serveRelationFanout-BuSmWawx.mjs}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RELATION_FUNCTION_PREFIX } from './ADMIN_FUNCTIONS-
|
|
1
|
+
import { RELATION_FUNCTION_PREFIX } from './ADMIN_FUNCTIONS-BvGiv2u6.mjs';
|
|
2
2
|
|
|
3
3
|
const serveRelationFanout = async (schema, database, functionPath, args) => {
|
|
4
4
|
const table = typeof args["table"] === "string" ? args["table"] : "";
|