@spooky-sync/core 0.0.1-canary.221 → 0.0.1-canary.222

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.js CHANGED
@@ -3473,6 +3473,14 @@ function phaseStatOf(samples, lastMs) {
3473
3473
  count: samples.length
3474
3474
  };
3475
3475
  }
3476
+ /**
3477
+ * Fraction of a query's TTL after which the client refreshes `lastActiveAt`.
3478
+ *
3479
+ * Must leave room for at least one retry: the server sweeps a view — and its
3480
+ * `_00_list_ref` edges — the moment `lastActiveAt + ttl` passes, and the client
3481
+ * is LIVE on those edges, so a swept-but-still-watched view empties the UI.
3482
+ */
3483
+ const TTL_HEARTBEAT_FRACTION = .5;
3476
3484
  var DataModule = class DataModule {
3477
3485
  /** Tab identity baked into mutation ids (shared-tabs rollback routing);
3478
3486
  * undefined in solo mode, where mutation-id falls back to a session id. */
@@ -5023,7 +5031,7 @@ var DataModule = class DataModule {
5023
5031
  }
5024
5032
  startTTLHeartbeat(queryState, hash) {
5025
5033
  if (queryState.ttlTimer) return;
5026
- const heartbeatTime = Math.floor(queryState.ttlDurationMs * .9);
5034
+ const heartbeatTime = Math.floor(queryState.ttlDurationMs * TTL_HEARTBEAT_FRACTION);
5027
5035
  queryState.ttlTimer = setTimeout(() => {
5028
5036
  queryState.ttlTimer = null;
5029
5037
  if ((this.subscriptions.get(hash)?.size ?? 0) === 0) {
@@ -7735,8 +7743,8 @@ function selfAllowlistedVariant(flag, userId) {
7735
7743
 
7736
7744
  //#endregion
7737
7745
  //#region src/modules/devtools/index.ts
7738
- const CORE_VERSION = "0.0.1-canary.221";
7739
- const WASM_VERSION = "0.0.1-canary.221";
7746
+ const CORE_VERSION = "0.0.1-canary.222";
7747
+ const WASM_VERSION = "0.0.1-canary.222";
7740
7748
  const SURREAL_VERSION = "3.0.3";
7741
7749
  var DevToolsService = class DevToolsService {
7742
7750
  eventsHistory = [];
@@ -12751,7 +12759,7 @@ var Sp00kyClient = class {
12751
12759
  return new TabsCoordinator({
12752
12760
  tabId,
12753
12761
  fingerprint: computeTabsFingerprint({
12754
- coreVersion: "0.0.1-canary.221",
12762
+ coreVersion: "0.0.1-canary.222",
12755
12763
  schemaHash: hash53(this.config.schemaSurql),
12756
12764
  endpoint: this.config.database.endpoint ?? "",
12757
12765
  namespace: this.config.database.namespace,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spooky-sync/core",
3
- "version": "0.0.1-canary.221",
3
+ "version": "0.0.1-canary.222",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -60,8 +60,8 @@
60
60
  }
61
61
  },
62
62
  "dependencies": {
63
- "@spooky-sync/query-builder": "0.0.1-canary.221",
64
- "@spooky-sync/ssp-wasm": "0.0.1-canary.221",
63
+ "@spooky-sync/query-builder": "0.0.1-canary.222",
64
+ "@spooky-sync/ssp-wasm": "0.0.1-canary.222",
65
65
  "@sqlite.org/sqlite-wasm": "3.53.0-build1",
66
66
  "@surrealdb/wasm": "^3.0.3",
67
67
  "blurhash": "^2.0.5",
@@ -90,6 +90,15 @@ export interface DurableMembership {
90
90
  confirmed: boolean;
91
91
  }
92
92
 
93
+ /**
94
+ * Fraction of a query's TTL after which the client refreshes `lastActiveAt`.
95
+ *
96
+ * Must leave room for at least one retry: the server sweeps a view — and its
97
+ * `_00_list_ref` edges — the moment `lastActiveAt + ttl` passes, and the client
98
+ * is LIVE on those edges, so a swept-but-still-watched view empties the UI.
99
+ */
100
+ const TTL_HEARTBEAT_FRACTION = 0.5;
101
+
93
102
  export class DataModule<S extends SchemaStructure> {
94
103
  /** Tab identity baked into mutation ids (shared-tabs rollback routing);
95
104
  * undefined in solo mode, where mutation-id falls back to a session id. */
@@ -2452,7 +2461,25 @@ export class DataModule<S extends SchemaStructure> {
2452
2461
  private startTTLHeartbeat(queryState: QueryState, hash: QueryHash): void {
2453
2462
  if (queryState.ttlTimer) return;
2454
2463
 
2455
- const heartbeatTime = Math.floor(queryState.ttlDurationMs * 0.9);
2464
+ // Half the TTL, not 0.9 of it, so ONE failed beat is survivable.
2465
+ //
2466
+ // The timer re-arms after each beat, so expiry sits a full TTL after the
2467
+ // last SUCCESSFUL refresh. At 0.9 a beat that fails — the SSP restarting,
2468
+ // a reconnect, a slow database — left 10% of the TTL before the sweep
2469
+ // deleted the row AND its `_00_list_ref` edges, with no second attempt in
2470
+ // between. On a 10m TTL that is 60 seconds of slack against an SSP
2471
+ // bootstrap that takes ~2 minutes on a large tenant, so the view lost a
2472
+ // race it could not win.
2473
+ //
2474
+ // The user-visible form is content vanishing and coming back: the client
2475
+ // is LIVE on those edges, so it sees the sweep's deletes immediately, and
2476
+ // only restores them once its heartbeat notices the row is gone and
2477
+ // re-registers. Reported as chat messages disappearing for 10+ seconds.
2478
+ //
2479
+ // At 0.5 a single failure still leaves another attempt and half the TTL.
2480
+ // The cost is one extra beat per view per TTL, which is a single
2481
+ // `UPDATE ... SET lastActiveAt`.
2482
+ const heartbeatTime = Math.floor(queryState.ttlDurationMs * TTL_HEARTBEAT_FRACTION);
2456
2483
 
2457
2484
  queryState.ttlTimer = setTimeout(() => {
2458
2485
  queryState.ttlTimer = null;