@lunora/do 1.0.0-alpha.16 → 1.0.0-alpha.17
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
|
@@ -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. */
|
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. */
|
package/dist/index.mjs
CHANGED
|
@@ -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-DDp_NomS.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';
|
|
@@ -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,
|
|
@@ -376,6 +392,8 @@ class OwnerRelay extends RelayLink {
|
|
|
376
392
|
relayShapeRegistry = /* @__PURE__ */ new Map();
|
|
377
393
|
/** 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
394
|
relayShapeProxies = /* @__PURE__ */ new Map();
|
|
395
|
+
/** 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. */
|
|
396
|
+
promotionState = "owned";
|
|
379
397
|
constructor(host, ownerKey) {
|
|
380
398
|
super(host, { ownerKey });
|
|
381
399
|
}
|
|
@@ -406,13 +424,22 @@ class OwnerRelay extends RelayLink {
|
|
|
406
424
|
}
|
|
407
425
|
/**
|
|
408
426
|
* 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
|
-
* `
|
|
427
|
+
* (plan 075 Phase 2, hysteresis added in Phase 4). `0` keeps every connection on
|
|
428
|
+
* the owner. The owner promotes once its live socket count reaches
|
|
429
|
+
* `LUNORA_RELAY_THRESHOLD` (`tUp`), fanning to a fixed `LUNORA_RELAY_FAN` (capped
|
|
430
|
+
* by `LUNORA_MAX_RELAYS`, the cost ceiling), and only collapses back to
|
|
431
|
+
* owner-served once subscribers drain below `LUNORA_RELAY_COLLAPSE_THRESHOLD`
|
|
432
|
+
* (`tDown`) — the band between the two holds the current state so a shard
|
|
433
|
+
* hovering near the threshold can't flap. {@link clampPromotionThresholds}
|
|
434
|
+
* guarantees a valid `tDown < tUp` band even under a misconfigured collapse
|
|
435
|
+
* threshold. Advances the promotion latch, so it is not a pure read.
|
|
412
436
|
*/
|
|
413
437
|
relayCount() {
|
|
414
|
-
const
|
|
415
|
-
|
|
438
|
+
const subscribers = this.host.getWebSockets().length;
|
|
439
|
+
const tUp = envPositiveInt(this.host.env(), "LUNORA_RELAY_THRESHOLD", DEFAULT_PROMOTION_THRESHOLDS.tUp);
|
|
440
|
+
const tDownRaw = envPositiveInt(this.host.env(), "LUNORA_RELAY_COLLAPSE_THRESHOLD", DEFAULT_PROMOTION_THRESHOLDS.tDown);
|
|
441
|
+
this.promotionState = nextPromotionState(this.promotionState, subscribers, clampPromotionThresholds(tUp, tDownRaw));
|
|
442
|
+
if (this.promotionState === "owned") {
|
|
416
443
|
return 0;
|
|
417
444
|
}
|
|
418
445
|
const maxRelays = envPositiveInt(this.host.env(), "LUNORA_MAX_RELAYS", DEFAULT_MAX_RELAYS);
|
|
@@ -497,7 +524,10 @@ class OwnerRelay extends RelayLink {
|
|
|
497
524
|
epoch,
|
|
498
525
|
fromCursor,
|
|
499
526
|
name: entry.name,
|
|
500
|
-
|
|
527
|
+
// Wire-encode before the poke crosses the owner→relay `JSON.stringify`
|
|
528
|
+
// hop (`requestRelayMessage`); the relay re-frames it with
|
|
529
|
+
// `preEncoded` so a `bytes`/`bigint` shape column isn't dropped/truncated.
|
|
530
|
+
rowsPatch: encodeRowsPatch(rowsPatch),
|
|
501
531
|
type: "relay_shape_poke"
|
|
502
532
|
};
|
|
503
533
|
for (const index of relays) {
|
|
@@ -541,7 +571,9 @@ class OwnerRelay extends RelayLink {
|
|
|
541
571
|
epoch,
|
|
542
572
|
fromCursor,
|
|
543
573
|
name: entry.name,
|
|
544
|
-
|
|
574
|
+
// Wire-encode before the owner→relay `JSON.stringify` hop (see the
|
|
575
|
+
// cohort-multicast path); the relay re-frames it with `preEncoded`.
|
|
576
|
+
rowsPatch: encodeRowsPatch(rowsPatch),
|
|
545
577
|
targetConnectionId: entry.connectionId,
|
|
546
578
|
type: "relay_shape_poke"
|
|
547
579
|
};
|
|
@@ -861,13 +893,19 @@ class RelayMember extends RelayLink {
|
|
|
861
893
|
if (memo?.cursor !== poke.fromCursor || memo.epoch !== poke.epoch || shapeRoutingKey(sub.name, sub.args) !== routingKey) {
|
|
862
894
|
continue;
|
|
863
895
|
}
|
|
864
|
-
const frames = buildPokeFrames(
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
896
|
+
const frames = buildPokeFrames(
|
|
897
|
+
[{ rowsPatch: poke.rowsPatch, shapeId: subId }],
|
|
898
|
+
{
|
|
899
|
+
baseCheckpoint: void 0,
|
|
900
|
+
checkpoint: poke.checkpoint,
|
|
901
|
+
epoch: poke.epoch,
|
|
902
|
+
lastMutationId: void 0,
|
|
903
|
+
pokeId: this.host.nextPokeId()
|
|
904
|
+
},
|
|
905
|
+
// `poke.rowsPatch` was wire-encoded by the owner before it crossed
|
|
906
|
+
// the hub — don't double-encode it here.
|
|
907
|
+
{ preEncoded: true }
|
|
908
|
+
);
|
|
871
909
|
for (const frame of frames) {
|
|
872
910
|
trySendFrame(ws, frame);
|
|
873
911
|
}
|
|
@@ -4651,8 +4689,12 @@ class ShardDO {
|
|
|
4651
4689
|
* subscriber counts are folded live from each socket's attachment via
|
|
4652
4690
|
* {@link summarizeFanoutTopics}; the running per-path cost counters are the
|
|
4653
4691
|
* in-memory {@link ShardDO.fanout} tallies, sharing `metrics.sinceMs` as the
|
|
4654
|
-
* "since this instance woke" epoch.
|
|
4655
|
-
*
|
|
4692
|
+
* "since this instance woke" epoch. Touches no SQLite and mutates no socket
|
|
4693
|
+
* state; it does call `relay.relayCount()`, which advances the promotion
|
|
4694
|
+
* latch — safe here because that transition is a pure, monotonic function of
|
|
4695
|
+
* the live socket count (DOs are single-threaded), so a metrics poll only ever
|
|
4696
|
+
* drives the latch to the same state the routing path would compute for the
|
|
4697
|
+
* same count, never a divergent one.
|
|
4656
4698
|
*/
|
|
4657
4699
|
collectFanoutMetrics() {
|
|
4658
4700
|
const summary = summarizeFanoutTopics(this.state.getWebSockets().map((ws) => this.readAttachment(ws)));
|