@spooky-sync/core 0.0.1-canary.246 → 0.0.1-canary.248
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 +12 -5
- package/package.json +3 -3
- package/src/sync/live.saga.test.ts +41 -0
- package/src/sync/live.saga.ts +22 -2
package/dist/index.js
CHANGED
|
@@ -1073,8 +1073,8 @@ function selfAllowlistedVariant(flag, userId) {
|
|
|
1073
1073
|
|
|
1074
1074
|
//#endregion
|
|
1075
1075
|
//#region src/modules/devtools/index.ts
|
|
1076
|
-
const CORE_VERSION = "0.0.1-canary.
|
|
1077
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
1076
|
+
const CORE_VERSION = "0.0.1-canary.248";
|
|
1077
|
+
const WASM_VERSION = "0.0.1-canary.248";
|
|
1078
1078
|
const SURREAL_VERSION = "3.0.3";
|
|
1079
1079
|
var DevToolsService = class DevToolsService {
|
|
1080
1080
|
eventsHistory = [];
|
|
@@ -5098,9 +5098,16 @@ function* liveInvalidate() {
|
|
|
5098
5098
|
}
|
|
5099
5099
|
/** One or more edges of these queries changed on the server. */
|
|
5100
5100
|
function* liveChange(env, hashes, rows) {
|
|
5101
|
-
const [known, role] = yield fx.state.read((s) => [
|
|
5101
|
+
const [known, role, streak] = yield fx.state.read((s) => [
|
|
5102
|
+
hashes.filter((h) => s.queries.has(h)),
|
|
5103
|
+
s.tabRole,
|
|
5104
|
+
s.sync.pollIdleStreak
|
|
5105
|
+
]);
|
|
5102
5106
|
if (known.length === 0) return;
|
|
5103
|
-
|
|
5107
|
+
if (streak > 0) {
|
|
5108
|
+
yield fx.state.update(patchSync({ pollIdleStreak: 0 }));
|
|
5109
|
+
yield fx.timer.set("poll", env.pollBaseMs, { type: "PollTick" });
|
|
5110
|
+
}
|
|
5104
5111
|
if (rows && rows.length > 0) yield* landInlineRows(env, rows);
|
|
5105
5112
|
yield* markMembershipDirty(known);
|
|
5106
5113
|
if (role === "leader") yield fx.emit({
|
|
@@ -12509,7 +12516,7 @@ function createTabsCoordinator(config, s, host) {
|
|
|
12509
12516
|
return new TabsCoordinator({
|
|
12510
12517
|
tabId: s.tabId,
|
|
12511
12518
|
fingerprint: computeTabsFingerprint({
|
|
12512
|
-
coreVersion: "0.0.1-canary.
|
|
12519
|
+
coreVersion: "0.0.1-canary.248",
|
|
12513
12520
|
schemaHash: hash53(config.schemaSurql),
|
|
12514
12521
|
endpoint: config.database.endpoint ?? "",
|
|
12515
12522
|
namespace: 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.
|
|
3
|
+
"version": "0.0.1-canary.248",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@spooky-sync/query-builder": "0.0.1-canary.
|
|
67
|
-
"@spooky-sync/ssp-wasm": "0.0.1-canary.
|
|
66
|
+
"@spooky-sync/query-builder": "0.0.1-canary.248",
|
|
67
|
+
"@spooky-sync/ssp-wasm": "0.0.1-canary.248",
|
|
68
68
|
"@sqlite.org/sqlite-wasm": "3.53.0-build1",
|
|
69
69
|
"@surrealdb/wasm": "^3.0.3",
|
|
70
70
|
"blurhash": "^2.0.5",
|
|
@@ -68,6 +68,28 @@ describe('liveChange', () => {
|
|
|
68
68
|
const none = await runPure(liveChange(env, ['zz']), { state: s });
|
|
69
69
|
expect(none.timers.size).toBe(0);
|
|
70
70
|
});
|
|
71
|
+
|
|
72
|
+
it('pulls a backed-off poll back to the base cadence, and leaves an already-base one alone', async () => {
|
|
73
|
+
const entry = buildEntry({ def: { hash: 'a' } });
|
|
74
|
+
// Coasted up to the cap: the armed timer is the reason a change the poll
|
|
75
|
+
// has to catch could wait 5s, so the event has to re-arm it, not just zero
|
|
76
|
+
// the streak the next tick would read.
|
|
77
|
+
const backedOff = R.patchSync({ pollIdleStreak: 4 })(buildState([entry]));
|
|
78
|
+
const out = await runPure(liveChange(env, ['a']), { state: backedOff });
|
|
79
|
+
expect(out.timers.get('poll')).toEqual({ ms: env.pollBaseMs, event: { type: 'PollTick' } });
|
|
80
|
+
|
|
81
|
+
// Already at base: re-arming here would push the poll out by another
|
|
82
|
+
// `pollBaseMs` on every notification and starve it under sustained LIVE.
|
|
83
|
+
const atBase = R.patchSync({ pollIdleStreak: 0 })(buildState([entry]));
|
|
84
|
+
const quiet = await runPure(liveChange(env, ['a']), { state: atBase });
|
|
85
|
+
expect(quiet.timers.has('poll')).toBe(false);
|
|
86
|
+
expect(quiet.state.membershipDirty.has('a')).toBe(true);
|
|
87
|
+
|
|
88
|
+
// An unknown hash is not activity: nothing is touched.
|
|
89
|
+
const unknown = await runPure(liveChange(env, ['zz']), { state: backedOff });
|
|
90
|
+
expect(unknown.timers.has('poll')).toBe(false);
|
|
91
|
+
expect(unknown.state.sync.pollIdleStreak).toBe(4);
|
|
92
|
+
});
|
|
71
93
|
});
|
|
72
94
|
|
|
73
95
|
describe('rowOfEdge', () => {
|
|
@@ -120,6 +142,25 @@ describe('liveChange with the body joined on', () => {
|
|
|
120
142
|
expect([...out.state.membershipDirty]).toEqual(['a']);
|
|
121
143
|
});
|
|
122
144
|
|
|
145
|
+
it('leaves a failed inline body for the fetch path without advancing its version', async () => {
|
|
146
|
+
const s = buildState([buildEntry({ def: { hash: 'a' } })]);
|
|
147
|
+
const out = await runPure(liveChange(env, ['a'], [row(7)]), {
|
|
148
|
+
state: s,
|
|
149
|
+
handlers: {
|
|
150
|
+
'local.execute': () => { throw new Error('stale local epoch'); },
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
expect(out.state.versions.has('game:g1')).toBe(false);
|
|
154
|
+
expect([...out.state.membershipDirty]).toEqual(['a']);
|
|
155
|
+
expect(out.log.filter((e) => e.kind === 'ssp.ingest')).toHaveLength(0);
|
|
156
|
+
expect(out.emitted).toContainEqual({
|
|
157
|
+
type: 'log',
|
|
158
|
+
level: 'debug',
|
|
159
|
+
message: 'inline live body not landed; the fetch path will pull it',
|
|
160
|
+
data: { ids: ['game:g1'] },
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
123
164
|
it('writes one copy when the same row arrives for several views', async () => {
|
|
124
165
|
const s = buildState([buildEntry({ def: { hash: 'a' } })]);
|
|
125
166
|
const out = await runPure(liveChange(env, ['a'], [row(7), row(7), row(8)]), { state: s, handlers });
|
package/src/sync/live.saga.ts
CHANGED
|
@@ -47,9 +47,29 @@ export function* liveInvalidate(): Saga<void> {
|
|
|
47
47
|
|
|
48
48
|
/** One or more edges of these queries changed on the server. */
|
|
49
49
|
export function* liveChange(env: SagaEnv, hashes: QueryHash[], rows?: InlineRow[]): Saga<void> {
|
|
50
|
-
const [known, role] = (yield fx.state.read((s) => [hashes.filter((h) => s.queries.has(h)), s.tabRole])) as [
|
|
50
|
+
const [known, role, streak] = (yield fx.state.read((s) => [hashes.filter((h) => s.queries.has(h)), s.tabRole, s.sync.pollIdleStreak])) as [
|
|
51
|
+
QueryHash[],
|
|
52
|
+
ClientState['tabRole'],
|
|
53
|
+
number,
|
|
54
|
+
];
|
|
51
55
|
if (known.length === 0) return;
|
|
52
|
-
|
|
56
|
+
// Zeroing the streak only decides the delay the NEXT tick picks: `pollTick`
|
|
57
|
+
// arms its timer at the end of a tick, so a page that had coasted up to the
|
|
58
|
+
// 5s cap still waited out that armed timer. When the poll is the thing that
|
|
59
|
+
// catches a change (a notification the server dropped, a row LIVE never
|
|
60
|
+
// carried) that made the worst case 5s even though LIVE had just proved the
|
|
61
|
+
// page was active. Re-arm at the base cadence too, so the safety net is back
|
|
62
|
+
// under us immediately.
|
|
63
|
+
//
|
|
64
|
+
// Only when the poll had actually backed off. A `set` on the same key
|
|
65
|
+
// replaces the pending timer, so re-arming on every event while LIVE is
|
|
66
|
+
// delivering faster than `pollBaseMs` would push the poll out forever and
|
|
67
|
+
// starve the full-membership reconciliation it exists to provide. A streak
|
|
68
|
+
// of 0 means the cadence is already base and there is nothing to correct.
|
|
69
|
+
if (streak > 0) {
|
|
70
|
+
yield fx.state.update(R.patchSync({ pollIdleStreak: 0 }));
|
|
71
|
+
yield fx.timer.set('poll', env.pollBaseMs, { type: 'PollTick' });
|
|
72
|
+
}
|
|
53
73
|
// Before marking membership dirty: recording the version here is what makes
|
|
54
74
|
// the re-read's `planFetch` find nothing left to pull for this row.
|
|
55
75
|
if (rows && rows.length > 0) yield* landInlineRows(env, rows);
|