@doync/client 0.1.0 → 0.1.1
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/README.md +1 -17
- package/dist/adapter.cjs +1 -1
- package/dist/adapter.d.cts +1 -1
- package/dist/adapter.d.ts +1 -1
- package/dist/adapter.js +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/client-Bv6QMenB.cjs +15 -0
- package/dist/client-Db0Jfwuy.js +16 -0
- package/dist/client-Db0Jfwuy.js.map +1 -0
- package/dist/{client-CNyLMCw0.d.ts → client-OAVaC3pt.d.ts} +79 -91
- package/dist/client-OAVaC3pt.d.ts.map +1 -0
- package/dist/{client-DHXO0dbf.d.cts → client-OPt8Axyp.d.cts} +79 -91
- package/dist/client-OPt8Axyp.d.cts.map +1 -0
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/internal.cjs +1 -1
- package/dist/internal.d.cts +3 -12
- package/dist/internal.d.cts.map +1 -1
- package/dist/internal.d.ts +3 -12
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -1
- package/package.json +3 -3
- package/src/client.ts +82 -32
- package/src/engine.ts +188 -134
- package/src/index.ts +1 -6
- package/src/internal.ts +4 -4
- package/src/migrate.ts +1 -1
- package/src/replica/db/0002_prefs_table.sql +9 -0
- package/src/replica/db/meta/0002_snapshot.json +191 -0
- package/src/replica/db/meta/_journal.json +7 -0
- package/src/replica/db/schema.ts +12 -3
- package/src/replica/index.ts +5 -4
- package/src/replica/meta.ts +23 -0
- package/src/replica/track.ts +20 -39
- package/src/socket-reconnect.ts +29 -5
- package/dist/client-C6jAdhbe.cjs +0 -15
- package/dist/client-CNyLMCw0.d.ts.map +0 -1
- package/dist/client-ClV8ce6X.js +0 -16
- package/dist/client-ClV8ce6X.js.map +0 -1
- package/dist/client-DHXO0dbf.d.cts.map +0 -1
- package/src/client-mutation-registry.ts +0 -31
- package/src/mutations.ts +0 -96
package/src/engine.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
AuthContext,
|
|
2
3
|
BoundQuery,
|
|
3
4
|
DoyncSchema,
|
|
4
5
|
MutationDefinition,
|
|
6
|
+
MutationExec,
|
|
5
7
|
RowDecoder,
|
|
6
8
|
SqlValue,
|
|
7
9
|
} from '@doync/core'
|
|
@@ -18,21 +20,18 @@ import type {
|
|
|
18
20
|
|
|
19
21
|
import { isBoundQuery } from '@doync/core/internal'
|
|
20
22
|
import {
|
|
21
|
-
assertWireRepresentable,
|
|
22
|
-
canonicalizeArgsBinary,
|
|
23
23
|
declaredSchemaVersion,
|
|
24
24
|
decodeArgs,
|
|
25
25
|
decodeImageValue,
|
|
26
26
|
encodeArgs,
|
|
27
27
|
isInternalTable,
|
|
28
|
+
prepareArgs,
|
|
28
29
|
} from '@doync/core/internal'
|
|
29
30
|
|
|
30
|
-
import type { ClientMutationRegistry, ClientMutationTx } from './mutations'
|
|
31
31
|
import type { LocalDb } from './port'
|
|
32
32
|
import type { SeamStatus, SyncSocket } from './socket'
|
|
33
33
|
|
|
34
|
-
import {
|
|
35
|
-
import { RawRead, type LocalStatement } from './raw-read'
|
|
34
|
+
import { type LocalStatement, RawRead } from './raw-read'
|
|
36
35
|
import {
|
|
37
36
|
applyBundledMigrations,
|
|
38
37
|
assertNoReservedTables,
|
|
@@ -47,12 +46,13 @@ import {
|
|
|
47
46
|
quoteIdent,
|
|
48
47
|
readMeta,
|
|
49
48
|
readReleaseStamp,
|
|
49
|
+
type ReleaseStamp,
|
|
50
50
|
replayMigrations,
|
|
51
51
|
requireTable,
|
|
52
52
|
resolveReleaseTtlMs,
|
|
53
53
|
upsertReleaseStamp,
|
|
54
54
|
writeMeta,
|
|
55
|
-
|
|
55
|
+
writePref,
|
|
56
56
|
} from './replica'
|
|
57
57
|
|
|
58
58
|
/**
|
|
@@ -76,12 +76,12 @@ export interface MutationResult {
|
|
|
76
76
|
export type LogoutBehavior = 'keep' | 'forget'
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
* The `
|
|
79
|
+
* The `__doync_prefs` key the durable {@link LogoutBehavior} lives under
|
|
80
80
|
* (closeio/doync#135) — shared by the direct engine's
|
|
81
81
|
* {@link ClientEngine.setLogoutBehavior} / `createClient` write and the web DB
|
|
82
82
|
* worker's per-identity write + boot read, so the ends can never drift.
|
|
83
83
|
*/
|
|
84
|
-
export const
|
|
84
|
+
export const __LOGOUT_BEHAVIOR_PREF_KEY = 'logout_behavior'
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* The `__doync_meta` key the Client's durable connected-time counter lives
|
|
@@ -99,12 +99,22 @@ export const __CONNECTED_CLOCK_META_KEY = 'connected_clock'
|
|
|
99
99
|
* after deploying a matching client. Terminal until reload.
|
|
100
100
|
* - `server-behind` — client is ahead of a mid-deploy Mirror; the client backs
|
|
101
101
|
* off and re-handshakes automatically.
|
|
102
|
-
* - `resync` — local
|
|
103
|
-
* clientId
|
|
102
|
+
* - `resync` — local sync state wiped and rebuilding as a fresh Client under the
|
|
103
|
+
* same identity (pending writes dropped, new clientId). Transient; clears
|
|
104
|
+
* when sync resumes.
|
|
105
|
+
* - `reaped` — same fresh-client rebuild as `resync`, but the Origin forgot this
|
|
106
|
+
* Client (gone past the client-state lifetime). Transient; clears when sync
|
|
107
|
+
* resumes. Distinct so the app can say "you were away too long" rather than
|
|
108
|
+
* "data refreshed".
|
|
104
109
|
* - `forget` — local store erased (including identity); boots as a fresh client.
|
|
105
110
|
* Transient; clears when sync resumes.
|
|
106
111
|
*/
|
|
107
|
-
export type SchemaEventKind =
|
|
112
|
+
export type SchemaEventKind =
|
|
113
|
+
| 'reload'
|
|
114
|
+
| 'server-behind'
|
|
115
|
+
| 'resync'
|
|
116
|
+
| 'reaped'
|
|
117
|
+
| 'forget'
|
|
108
118
|
|
|
109
119
|
/**
|
|
110
120
|
* One schema-status transition: `kind` plus a human-readable `message` for
|
|
@@ -274,17 +284,18 @@ export interface DoyncClient {
|
|
|
274
284
|
*/
|
|
275
285
|
onSchemaChange(listener: () => void): () => void
|
|
276
286
|
/**
|
|
277
|
-
*
|
|
278
|
-
*
|
|
287
|
+
* Rebuild sync state as a fresh Client under the same identity: drop pending
|
|
288
|
+
* writes, mint a new clientId, wipe the replica, and rebootstrap. Destructive
|
|
289
|
+
* to unsent work — not a harmless refresh. Optional on the interface —
|
|
279
290
|
* platform clients implement it.
|
|
280
291
|
*/
|
|
281
|
-
resync
|
|
292
|
+
resync(): void
|
|
282
293
|
/**
|
|
283
294
|
* Erase this identity's local data (replica, pendings, identity). Privacy /
|
|
284
295
|
* logout-forget path. Optional on the interface — platform clients implement
|
|
285
296
|
* it.
|
|
286
297
|
*/
|
|
287
|
-
forget
|
|
298
|
+
forget(identity?: string): void
|
|
288
299
|
/**
|
|
289
300
|
* Durably set {@link LogoutBehavior} at runtime (e.g. a "remember me"
|
|
290
301
|
* checkbox). Survives restart. Optional — platform clients implement it.
|
|
@@ -363,15 +374,15 @@ export interface ClientEngineConfig {
|
|
|
363
374
|
* Named mutation bodies resolved on `push` — deterministic, DB-only
|
|
364
375
|
* (ADR-0017/0021).
|
|
365
376
|
*/
|
|
366
|
-
readonly mutations:
|
|
377
|
+
readonly mutations: Record<string, MutationDefinition>
|
|
367
378
|
/** The Mirror socket seam (ADR-0016). */
|
|
368
379
|
readonly socket: SyncSocket
|
|
369
380
|
/**
|
|
370
381
|
* The asserted projected auth context (ADR-0018 addendum / closeio/doync#167)
|
|
371
382
|
* queries and mutation bodies read. Consumer-owned; the library ships no
|
|
372
|
-
* decode.
|
|
383
|
+
* decode. `null` when anonymous.
|
|
373
384
|
*/
|
|
374
|
-
readonly ctx?:
|
|
385
|
+
readonly ctx?: AuthContext
|
|
375
386
|
/**
|
|
376
387
|
* Transport credential PRESENTED as `connect{jwt}` (ADR-0016/0018) — the
|
|
377
388
|
* Mirror resolves token-first. Auth must ride the HANDSHAKE, not only the
|
|
@@ -678,13 +689,13 @@ class ViewHandle implements View<Record<string, unknown>> {
|
|
|
678
689
|
export class ClientEngine implements DoyncClient {
|
|
679
690
|
readonly #db: LocalDb
|
|
680
691
|
readonly #schema: DoyncSchema
|
|
681
|
-
readonly #mutations:
|
|
692
|
+
readonly #mutations: Record<string, MutationDefinition>
|
|
682
693
|
readonly #socket: SyncSocket
|
|
683
694
|
/**
|
|
684
695
|
* Projected auth ctx (ADR-0018) for queries/bodies. Updated by
|
|
685
|
-
* {@link updateAuth} on same-user refresh (#102).
|
|
696
|
+
* {@link updateAuth} on same-user refresh (#102). `null` when anonymous.
|
|
686
697
|
*/
|
|
687
|
-
#ctx:
|
|
698
|
+
#ctx: AuthContext
|
|
688
699
|
/**
|
|
689
700
|
* Bearer for every handshake (ADR-0016/0018). Updated by {@link updateAuth} so
|
|
690
701
|
* refresh rides in-band update AND the next reconnect (#102).
|
|
@@ -850,7 +861,7 @@ export class ClientEngine implements DoyncClient {
|
|
|
850
861
|
this.#schema = config.schema
|
|
851
862
|
this.#mutations = config.mutations
|
|
852
863
|
this.#socket = config.socket
|
|
853
|
-
this.#ctx = config.ctx ??
|
|
864
|
+
this.#ctx = config.ctx ?? null
|
|
854
865
|
this.#token = config.token
|
|
855
866
|
// Asserted identity (#167) — never from the token.
|
|
856
867
|
this.#userId = config.userId ?? null
|
|
@@ -953,19 +964,12 @@ export class ClientEngine implements DoyncClient {
|
|
|
953
964
|
// rebuilds engine tables and flags consumer wipe-and-resync below.
|
|
954
965
|
const { rolledBack } = createEngineTables(db)
|
|
955
966
|
if (rolledBack) {
|
|
956
|
-
// Consumer half of wipe-and-resync
|
|
957
|
-
//
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
applyBundledMigrations(
|
|
962
|
-
db,
|
|
963
|
-
this.#schema,
|
|
964
|
-
0,
|
|
965
|
-
declaredSchemaVersion(this.#schema),
|
|
966
|
-
)
|
|
967
|
+
// Consumer half of wipe-and-resync. Boot mints a fresh clientId because
|
|
968
|
+
// the stored one goes with the rest of the cookie/cursor state. The
|
|
969
|
+
// bundle version is passed explicitly — `#bundleVersion` is assigned
|
|
970
|
+
// below, after the track settles.
|
|
971
|
+
this.#wipeStore(db, declaredSchemaVersion(this.#schema))
|
|
967
972
|
this.#cookie = null
|
|
968
|
-
db.exec('DELETE FROM __doync_meta WHERE k = ?', 'cookie')
|
|
969
973
|
} else {
|
|
970
974
|
replayMigrations(db, this.#schema)
|
|
971
975
|
}
|
|
@@ -1262,7 +1266,7 @@ export class ClientEngine implements DoyncClient {
|
|
|
1262
1266
|
try {
|
|
1263
1267
|
const entry = this.#mutations[p.name]
|
|
1264
1268
|
if (entry === undefined) throw new Error(`unknown mutation "${p.name}"`)
|
|
1265
|
-
running =
|
|
1269
|
+
running = entry.body({ args: p.args, ctx: this.#ctx, sql: this.#tx() })
|
|
1266
1270
|
} catch (error) {
|
|
1267
1271
|
this.#failBody(p.mutationId, error)
|
|
1268
1272
|
return
|
|
@@ -1290,8 +1294,16 @@ export class ClientEngine implements DoyncClient {
|
|
|
1290
1294
|
}
|
|
1291
1295
|
|
|
1292
1296
|
/** Mutation-body write surface (exec returns rows, ADR-0021). */
|
|
1293
|
-
#tx():
|
|
1294
|
-
|
|
1297
|
+
#tx(): MutationExec {
|
|
1298
|
+
// Booleans canonicalize to 1/0 at the seam — the LocalDb port speaks
|
|
1299
|
+
// SqlValue only (SQLite has no boolean affinity).
|
|
1300
|
+
return {
|
|
1301
|
+
exec: (query, ...params) =>
|
|
1302
|
+
this.#db.exec(
|
|
1303
|
+
query,
|
|
1304
|
+
...params.map((p) => (typeof p === 'boolean' ? (p ? 1 : 0) : p)),
|
|
1305
|
+
),
|
|
1306
|
+
}
|
|
1295
1307
|
}
|
|
1296
1308
|
|
|
1297
1309
|
/**
|
|
@@ -1337,25 +1349,15 @@ export class ClientEngine implements DoyncClient {
|
|
|
1337
1349
|
if (entry === undefined) {
|
|
1338
1350
|
return settledRejection(new Error(`doync: unknown mutation "${name}"`))
|
|
1339
1351
|
}
|
|
1340
|
-
const registered = asClientMutation(entry)
|
|
1341
1352
|
let liveArgs: unknown
|
|
1342
1353
|
let wireArgs: unknown
|
|
1343
1354
|
try {
|
|
1344
|
-
//
|
|
1345
|
-
//
|
|
1346
|
-
//
|
|
1347
|
-
// schema
|
|
1348
|
-
//
|
|
1349
|
-
|
|
1350
|
-
// TypedArray/DataView → ArrayBuffer before schema + body (same as Origin
|
|
1351
|
-
// / query resolve). Encode the post-schema value so wire and durable
|
|
1352
|
-
// queue match what the optimistic body saw (validators assumed
|
|
1353
|
-
// pass-through / idempotent).
|
|
1354
|
-
const canonical = canonicalizeArgsBinary(args)
|
|
1355
|
-
liveArgs =
|
|
1356
|
-
registered.args !== undefined
|
|
1357
|
-
? validateMutationArgs(registered.args, canonical)
|
|
1358
|
-
: canonical
|
|
1355
|
+
// The one args-preparation path (ADR-0031), shared with the Origin push
|
|
1356
|
+
// seam: guard, canonicalize, validate through the definition's schema.
|
|
1357
|
+
// Throw is sync fail-fast: nothing enqueued, `client` rejects. Encode
|
|
1358
|
+
// the post-schema value so wire and durable queue match what the
|
|
1359
|
+
// optimistic body saw (validators assumed pass-through / idempotent).
|
|
1360
|
+
liveArgs = prepareArgs(entry.args, args, 'mutation args')
|
|
1359
1361
|
wireArgs = encodeArgs(liveArgs, 'mutation args')
|
|
1360
1362
|
} catch (error) {
|
|
1361
1363
|
return settledRejection(error)
|
|
@@ -1464,19 +1466,19 @@ export class ClientEngine implements DoyncClient {
|
|
|
1464
1466
|
// --- auth (ADR-0018, closeio/doync#102) -----------------------------------
|
|
1465
1467
|
|
|
1466
1468
|
/**
|
|
1467
|
-
* Adopt a refreshed SAME-USER identity
|
|
1468
|
-
*
|
|
1469
|
-
*
|
|
1470
|
-
*
|
|
1471
|
-
*
|
|
1469
|
+
* Adopt a refreshed SAME-USER identity (#167). When connected with a bearer,
|
|
1470
|
+
* send in-band `updateAuth` (ADR-0018 / #85) so the Mirror extends auth
|
|
1471
|
+
* without reconnect when userId matches and `issuedAt` is newer. Identity
|
|
1472
|
+
* CHANGE (different `userId`, incl. anon↔user) is topology-level replica swap
|
|
1473
|
+
* — SharedWorker routes only same-user refreshes here.
|
|
1472
1474
|
*
|
|
1473
1475
|
* Token is stored for the next reconnect (#92: surviving SharedWorker must
|
|
1474
1476
|
* not reuse a stale token). `ctx`/`userId` asserted, never derived. Halted
|
|
1475
|
-
* engines never emit.
|
|
1477
|
+
* engines never emit. Pass `ctx: null` for anonymous.
|
|
1476
1478
|
*/
|
|
1477
1479
|
updateAuth(
|
|
1478
1480
|
token: string | null | undefined,
|
|
1479
|
-
ctx?:
|
|
1481
|
+
ctx?: AuthContext,
|
|
1480
1482
|
userId?: string | null,
|
|
1481
1483
|
): void {
|
|
1482
1484
|
this.#token = token ?? undefined
|
|
@@ -2147,7 +2149,7 @@ export class ClientEngine implements DoyncClient {
|
|
|
2147
2149
|
}
|
|
2148
2150
|
break
|
|
2149
2151
|
case 'pokeReject':
|
|
2150
|
-
this.#handleReject(message.mutationId, message.error)
|
|
2152
|
+
this.#handleReject(message.mutationId, message.error, message.reason)
|
|
2151
2153
|
break
|
|
2152
2154
|
case 'onceStart':
|
|
2153
2155
|
// Fresh Once accumulator (ADR-0012); reconnect re-issue resets partial.
|
|
@@ -2166,8 +2168,8 @@ export class ClientEngine implements DoyncClient {
|
|
|
2166
2168
|
this.#handleOnceEnd(message)
|
|
2167
2169
|
break
|
|
2168
2170
|
case 'resyncRequired':
|
|
2169
|
-
// heldAt-ahead tripwire (ADR-0029 / #225): resync self-heal
|
|
2170
|
-
//
|
|
2171
|
+
// heldAt-ahead tripwire (ADR-0029 / #225): resync self-heal as a fresh
|
|
2172
|
+
// client. First-class frame, not phrase-matched.
|
|
2171
2173
|
this.#errors.push(message.message)
|
|
2172
2174
|
this.#exclusive(() =>
|
|
2173
2175
|
this.#wipeAndResync(
|
|
@@ -2178,8 +2180,8 @@ export class ClientEngine implements DoyncClient {
|
|
|
2178
2180
|
case 'error':
|
|
2179
2181
|
this.#errors.push(message.message)
|
|
2180
2182
|
// Cookie-above-head (ADR-0020 addendum): wipe/redeploy makes every
|
|
2181
|
-
// returning cookie above head — heal via wipe-and-resync
|
|
2182
|
-
//
|
|
2183
|
+
// returning cookie above head — heal via wipe-and-resync as a fresh
|
|
2184
|
+
// client, not a permanent subscription error.
|
|
2183
2185
|
if (isCookieAboveHeadError(message.message)) {
|
|
2184
2186
|
this.#exclusive(() =>
|
|
2185
2187
|
this.#wipeAndResync(
|
|
@@ -2475,9 +2477,24 @@ export class ClientEngine implements DoyncClient {
|
|
|
2475
2477
|
* Rejection (ADR-0016/0019): mark rejected immediately (lmid cannot resolve
|
|
2476
2478
|
* it even under race), then exclusive-chain drop pending, reject `server`,
|
|
2477
2479
|
* re-project (optimistic effect vanishes).
|
|
2480
|
+
*
|
|
2481
|
+
* `reason: 'reaped'` (ADR-0015 / ADR-0037) is recovery, not a per-mutation
|
|
2482
|
+
* failure: the Origin has no cursor for this Client, so every pending is
|
|
2483
|
+
* dropped as a fresh-client heal — first-class tag, never phrase-matched.
|
|
2478
2484
|
*/
|
|
2479
|
-
#handleReject(mutationId: number, error: string): void {
|
|
2485
|
+
#handleReject(mutationId: number, error: string, reason?: 'reaped'): void {
|
|
2480
2486
|
this.#rejected.add(mutationId)
|
|
2487
|
+
if (reason === 'reaped') {
|
|
2488
|
+
const dropped = this.#pending.length
|
|
2489
|
+
this.#errors.push(error)
|
|
2490
|
+
this.#exclusive(() =>
|
|
2491
|
+
this.#wipeAndResync(
|
|
2492
|
+
`server forgot this client (reaped) — dropped ${dropped} pending write${dropped === 1 ? '' : 's'} — ${error}`,
|
|
2493
|
+
'reaped',
|
|
2494
|
+
),
|
|
2495
|
+
)
|
|
2496
|
+
return
|
|
2497
|
+
}
|
|
2481
2498
|
this.#exclusive(() =>
|
|
2482
2499
|
thenMaybe(this.#dropPending(mutationId), (touched) => {
|
|
2483
2500
|
this.#settleServer(mutationId, new Error(error))
|
|
@@ -2567,13 +2584,15 @@ export class ClientEngine implements DoyncClient {
|
|
|
2567
2584
|
)
|
|
2568
2585
|
}
|
|
2569
2586
|
|
|
2570
|
-
// --- recovery surface (ADR-0022)
|
|
2587
|
+
// --- recovery surface (ADR-0022 / ADR-0037) --------------------------------
|
|
2571
2588
|
|
|
2572
2589
|
/**
|
|
2573
|
-
* Rebuild sync state (ADR-0022
|
|
2574
|
-
*
|
|
2575
|
-
*
|
|
2576
|
-
*
|
|
2590
|
+
* Rebuild sync state as a fresh Client under the same identity (ADR-0022 /
|
|
2591
|
+
* ADR-0037 resync): drop pending writes, mint a new clientId, wipe replica /
|
|
2592
|
+
* memberships / cookie, rebootstrap. Same core as failed-migration,
|
|
2593
|
+
* cookie-above-head, and heldAt-ahead heals ({@link #wipeAndResync}).
|
|
2594
|
+
* Exclusive chain; offline rebootstrap rides next reconnect. Preferences
|
|
2595
|
+
* survive (`__doync_prefs`).
|
|
2577
2596
|
*/
|
|
2578
2597
|
resync(): void {
|
|
2579
2598
|
this.#exclusive(() =>
|
|
@@ -2583,10 +2602,10 @@ export class ClientEngine implements DoyncClient {
|
|
|
2583
2602
|
|
|
2584
2603
|
/**
|
|
2585
2604
|
* Erase this identity's local data (ADR-0022 forget): wipe replica,
|
|
2586
|
-
* memberships, pending, and
|
|
2587
|
-
*
|
|
2588
|
-
* rejected. Topology can
|
|
2589
|
-
* current store. Exclusive chain.
|
|
2605
|
+
* memberships, pending, meta, and preferences, then fresh first-sight under a
|
|
2606
|
+
* new clientId. Erases WHO, not just WHAT. Awaiters of forgotten writes are
|
|
2607
|
+
* rejected. Topology can erase the identity's database file on active forget
|
|
2608
|
+
* (#134); this resets the current store. Exclusive chain.
|
|
2590
2609
|
*/
|
|
2591
2610
|
forget(): void {
|
|
2592
2611
|
this.#exclusive(() =>
|
|
@@ -2598,24 +2617,24 @@ export class ClientEngine implements DoyncClient {
|
|
|
2598
2617
|
|
|
2599
2618
|
/**
|
|
2600
2619
|
* Durably set {@link LogoutBehavior} at runtime ({@link
|
|
2601
|
-
* DoyncClient.setLogoutBehavior}). Via {@link #
|
|
2620
|
+
* DoyncClient.setLogoutBehavior}). Via {@link #sideWritePref} so it commits
|
|
2602
2621
|
* outside the optimistic overlay (#135 / ADR-0019). Direct engine stores for
|
|
2603
2622
|
* RN; web DB worker reports to hub.
|
|
2604
2623
|
*/
|
|
2605
2624
|
setLogoutBehavior(behavior: LogoutBehavior): void {
|
|
2606
|
-
this.#
|
|
2625
|
+
this.#sideWritePref(__LOGOUT_BEHAVIOR_PREF_KEY, behavior)
|
|
2607
2626
|
}
|
|
2608
2627
|
|
|
2609
2628
|
/**
|
|
2610
|
-
* Durable `
|
|
2611
|
-
* frequency runtime
|
|
2612
|
-
* (connected clock #223) stay in memory and piggyback poke apply —
|
|
2613
|
-
* rebase per tick is wrong. Exclusive chain + {@link #rebase} so the
|
|
2614
|
-
* commits on the base, not the overlay SAVEPOINT.
|
|
2615
|
-
* set.
|
|
2629
|
+
* Durable `__doync_prefs` side write via rebase envelope (#139). For low-
|
|
2630
|
+
* frequency runtime preferences ({@link setLogoutBehavior}). High-frequency
|
|
2631
|
+
* values (connected clock #223) stay in memory and piggyback poke apply —
|
|
2632
|
+
* full rebase per tick is wrong. Exclusive chain + {@link #rebase} so the
|
|
2633
|
+
* write commits on the base, not the overlay SAVEPOINT. Pref-only → discard
|
|
2634
|
+
* touched set.
|
|
2616
2635
|
*/
|
|
2617
|
-
#
|
|
2618
|
-
this.#sideWrite(() =>
|
|
2636
|
+
#sideWritePref(key: string, value: string): void {
|
|
2637
|
+
this.#sideWrite(() => writePref(this.#db, key, value))
|
|
2619
2638
|
}
|
|
2620
2639
|
|
|
2621
2640
|
/**
|
|
@@ -2629,44 +2648,66 @@ export class ClientEngine implements DoyncClient {
|
|
|
2629
2648
|
}
|
|
2630
2649
|
|
|
2631
2650
|
/**
|
|
2632
|
-
*
|
|
2633
|
-
*
|
|
2634
|
-
*
|
|
2651
|
+
* Shared fresh-client store wipe for Resync, Forget, and the boot-time
|
|
2652
|
+
* ledger-rollback heal (ADR-0037): drop consumer tables, memberships, stamps,
|
|
2653
|
+
* pending queue, and all sync meta; re-replay the bundled track from empty.
|
|
2654
|
+
* Preferences (`__doync_prefs`) and the engine ledger stay. Caller mints the
|
|
2655
|
+
* fresh identity. FKs already off (ADR-0009).
|
|
2656
|
+
*
|
|
2657
|
+
* `bundleVersion` defaults to the engine's, and is passed explicitly from
|
|
2658
|
+
* boot, which runs before that field is assigned.
|
|
2635
2659
|
*/
|
|
2636
|
-
#
|
|
2660
|
+
#wipeStore(db: LocalDb, bundleVersion = this.#bundleVersion): void {
|
|
2637
2661
|
dropConsumerTables(db, this.#schema)
|
|
2638
2662
|
db.exec('DELETE FROM __doync_membership')
|
|
2639
2663
|
// Wipe stamps with memberships (ADR-0014 / #224); table shape stays.
|
|
2640
2664
|
clearReleaseStamps(db)
|
|
2641
2665
|
db.exec('DELETE FROM __doync_pending')
|
|
2642
|
-
// All meta gone
|
|
2666
|
+
// All sync meta gone — cookie, clientId, next-mutid, connected clock.
|
|
2643
2667
|
db.exec('DELETE FROM __doync_meta')
|
|
2644
2668
|
// Full bundled track from empty (re-records schema_version).
|
|
2645
|
-
applyBundledMigrations(db, this.#schema, 0,
|
|
2669
|
+
applyBundledMigrations(db, this.#schema, 0, bundleVersion)
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
/**
|
|
2673
|
+
* Store-erase core for forget / corrupt-meta heal: shared wipe plus
|
|
2674
|
+
* preferences. Caller mints identity. Topology may also erase the database
|
|
2675
|
+
* file.
|
|
2676
|
+
*/
|
|
2677
|
+
#forgetStore(db: LocalDb): void {
|
|
2678
|
+
this.#wipeStore(db)
|
|
2679
|
+
db.exec('DELETE FROM __doync_prefs')
|
|
2646
2680
|
}
|
|
2647
2681
|
|
|
2648
2682
|
/**
|
|
2649
|
-
*
|
|
2650
|
-
*
|
|
2651
|
-
*
|
|
2683
|
+
* Shared memory reset after a store wipe: reject awaiters, mint a fresh
|
|
2684
|
+
* clientId, empty the pending queue, null the cookie, reset the mutation
|
|
2685
|
+
* counter and connected clock.
|
|
2686
|
+
*/
|
|
2687
|
+
#resetToFreshClient(dropError: Error): void {
|
|
2688
|
+
const db = this.#db
|
|
2689
|
+
this.#rejectAllPending(dropError)
|
|
2690
|
+
this.#clientId = this.#generateId()
|
|
2691
|
+
writeMeta(db, 'client_id', this.#clientId)
|
|
2692
|
+
this.#cookie = null
|
|
2693
|
+
this.#nextMutationId = 1
|
|
2694
|
+
this.#connectedClock = 0
|
|
2695
|
+
this.#lastPongAt = null
|
|
2696
|
+
this.#pending = []
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
/**
|
|
2700
|
+
* Mid-session forget (ADR-0022): erase store (including preferences), mint
|
|
2701
|
+
* fresh identity, re-handshake (or next reconnect). Topology may also erase
|
|
2702
|
+
* the identity's database file.
|
|
2652
2703
|
*/
|
|
2653
2704
|
#forgetAndReset(reason: string): Awaitable<void> {
|
|
2654
2705
|
const db = this.#db
|
|
2655
2706
|
const touched = this.#rebase(() => {
|
|
2656
|
-
|
|
2657
|
-
this.#
|
|
2707
|
+
this.#forgetStore(db)
|
|
2708
|
+
this.#resetToFreshClient(
|
|
2658
2709
|
new Error(`doync: ${reason} — pending writes were forgotten`),
|
|
2659
2710
|
)
|
|
2660
|
-
this.#forgetStore(db)
|
|
2661
|
-
// Fresh first-sight identity; old server state lapses (ADR-0015).
|
|
2662
|
-
this.#clientId = this.#generateId()
|
|
2663
|
-
writeMeta(db, 'client_id', this.#clientId)
|
|
2664
|
-
this.#cookie = null
|
|
2665
|
-
this.#nextMutationId = 1
|
|
2666
|
-
// Fresh identity => zero connected-time (memory coherent with wiped store).
|
|
2667
|
-
this.#connectedClock = 0
|
|
2668
|
-
this.#lastPongAt = null
|
|
2669
|
-
this.#pending = []
|
|
2670
2711
|
})
|
|
2671
2712
|
return this.#rebootstrapAfterReset(
|
|
2672
2713
|
touched,
|
|
@@ -2677,8 +2718,9 @@ export class ClientEngine implements DoyncClient {
|
|
|
2677
2718
|
|
|
2678
2719
|
/**
|
|
2679
2720
|
* Rebootstrap tail for {@link #wipeAndResync} and {@link #forgetAndReset}: mark
|
|
2680
|
-
* fresh, clear instance info, emit event,
|
|
2681
|
-
* re-project all consumer
|
|
2721
|
+
* fresh, clear instance info, emit event, drop-and-redial when live so the
|
|
2722
|
+
* new clientId rides the upgrade pin (ADR-0016), re-project all consumer
|
|
2723
|
+
* tables. Offline tanks until the next connect (no live socket to drop).
|
|
2682
2724
|
*/
|
|
2683
2725
|
#rebootstrapAfterReset(
|
|
2684
2726
|
touched: Awaitable<Set<string>>,
|
|
@@ -2689,14 +2731,20 @@ export class ClientEngine implements DoyncClient {
|
|
|
2689
2731
|
this.#appliedVersion = this.#bundleVersion
|
|
2690
2732
|
this.#instanceInfo.clear()
|
|
2691
2733
|
this.#emitSchemaEvent(kind, message)
|
|
2692
|
-
|
|
2734
|
+
// Fresh clientId cannot ride an already-pinned socket (ADR-0016 upgrade
|
|
2735
|
+
// pin). Drop and redial so the next open re-handshakes as the new Client
|
|
2736
|
+
// — same posture as client-ahead skew backoff.
|
|
2737
|
+
if (this.#connected) {
|
|
2738
|
+
this.#setConnected(false)
|
|
2739
|
+
this.#socket.reconnect()
|
|
2740
|
+
}
|
|
2693
2741
|
this.#notifyChange(new Set([...t, ...this.#allConsumerTables()]))
|
|
2694
2742
|
})
|
|
2695
2743
|
}
|
|
2696
2744
|
|
|
2697
2745
|
/**
|
|
2698
|
-
* Reject every outstanding mutation promise (forget
|
|
2699
|
-
* Idempotent; orphaneds swallow — frees in-flight callers only.
|
|
2746
|
+
* Reject every outstanding mutation promise (resync/forget discard their
|
|
2747
|
+
* queue). Idempotent; orphaneds swallow — frees in-flight callers only.
|
|
2700
2748
|
*/
|
|
2701
2749
|
#rejectAllPending(error: Error): void {
|
|
2702
2750
|
// Safe: Map iteration skips removed keys; settleServer deletes when done.
|
|
@@ -2711,31 +2759,32 @@ export class ClientEngine implements DoyncClient {
|
|
|
2711
2759
|
}
|
|
2712
2760
|
|
|
2713
2761
|
/**
|
|
2714
|
-
* Wipe-replica-and-resync core (ADR-0020) for resync verb,
|
|
2715
|
-
*
|
|
2716
|
-
*
|
|
2717
|
-
*
|
|
2762
|
+
* Wipe-replica-and-resync core (ADR-0020 / ADR-0037) for the resync verb,
|
|
2763
|
+
* failed migration, cookie-above-head, heldAt-ahead, and client-state reap:
|
|
2764
|
+
* drop consumer tables, pending writes, and sync meta; mint a fresh clientId;
|
|
2765
|
+
* re-replay the bundle; rebootstrap. Preferences (`__doync_prefs`) and the
|
|
2766
|
+
* `__doync_engine` ledger survive. A clientId the Origin has never seen
|
|
2767
|
+
* starts its mutation counter at 1 by construction. `kind` is `reaped` when
|
|
2768
|
+
* the Origin forgot the Client so the consumer can distinguish that from an
|
|
2769
|
+
* ordinary repair; the default is `resync`.
|
|
2718
2770
|
*/
|
|
2719
|
-
#wipeAndResync(
|
|
2771
|
+
#wipeAndResync(
|
|
2772
|
+
reason: string,
|
|
2773
|
+
kind: 'resync' | 'reaped' = 'resync',
|
|
2774
|
+
): Awaitable<void> {
|
|
2720
2775
|
const db = this.#db
|
|
2721
2776
|
const touched = this.#rebase(() => {
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
clearReleaseStamps(db)
|
|
2727
|
-
// Full bundled track from empty base.
|
|
2728
|
-
writeMeta(db, 'schema_version', '0')
|
|
2729
|
-
applyBundledMigrations(db, this.#schema, 0, this.#bundleVersion)
|
|
2730
|
-
// Null cookie => full rebootstrap on handshake.
|
|
2731
|
-
this.#cookie = null
|
|
2732
|
-
db.exec('DELETE FROM __doync_meta WHERE k = ?', 'cookie')
|
|
2777
|
+
this.#wipeStore(db)
|
|
2778
|
+
this.#resetToFreshClient(
|
|
2779
|
+
new Error(`doync: ${reason} — pending writes were dropped`),
|
|
2780
|
+
)
|
|
2733
2781
|
})
|
|
2734
|
-
// Survivors re-push on rebootstrap.
|
|
2735
2782
|
return this.#rebootstrapAfterReset(
|
|
2736
2783
|
touched,
|
|
2737
|
-
|
|
2738
|
-
|
|
2784
|
+
kind,
|
|
2785
|
+
kind === 'reaped'
|
|
2786
|
+
? `client state was reaped — ${reason}`
|
|
2787
|
+
: `wiped and resyncing — ${reason}`,
|
|
2739
2788
|
)
|
|
2740
2789
|
}
|
|
2741
2790
|
|
|
@@ -2763,13 +2812,18 @@ export class ClientEngine implements DoyncClient {
|
|
|
2763
2812
|
|
|
2764
2813
|
/**
|
|
2765
2814
|
* Clear transient schema state once sync resumes (ack/poke after
|
|
2766
|
-
* re-handshake): server-behind / resync / forget. `reload` is
|
|
2767
|
-
* (halted ignores frames). Notifies subscribers; config callback has
|
|
2768
|
-
* cleared kind.
|
|
2815
|
+
* re-handshake): server-behind / resync / reaped / forget. `reload` is
|
|
2816
|
+
* terminal (halted ignores frames). Notifies subscribers; config callback has
|
|
2817
|
+
* no cleared kind.
|
|
2769
2818
|
*/
|
|
2770
2819
|
#clearTransientSchemaEvent(): void {
|
|
2771
2820
|
const kind = this.#schemaEvent?.kind
|
|
2772
|
-
if (
|
|
2821
|
+
if (
|
|
2822
|
+
kind === 'server-behind' ||
|
|
2823
|
+
kind === 'resync' ||
|
|
2824
|
+
kind === 'reaped' ||
|
|
2825
|
+
kind === 'forget'
|
|
2826
|
+
) {
|
|
2773
2827
|
this.#schemaEvent = null
|
|
2774
2828
|
this.#notifySchema()
|
|
2775
2829
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
export type { LogoutBehavior } from './client'
|
|
15
|
+
export type { AuthData } from '@doync/core'
|
|
15
16
|
export type {
|
|
16
17
|
ConnectionStatus,
|
|
17
18
|
DoyncClient,
|
|
@@ -28,9 +29,3 @@ export type {
|
|
|
28
29
|
View,
|
|
29
30
|
ViewStatus,
|
|
30
31
|
} from './engine'
|
|
31
|
-
export type {
|
|
32
|
-
ClientMutation,
|
|
33
|
-
ClientMutationHandler,
|
|
34
|
-
ClientMutationRegistry,
|
|
35
|
-
ClientMutationTx,
|
|
36
|
-
} from './mutations'
|
package/src/internal.ts
CHANGED
|
@@ -7,12 +7,11 @@
|
|
|
7
7
|
* entries, not here (no double-listing).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
export { createClientEngine } from './client'
|
|
11
|
-
export { toClientMutationRegistry } from './client-mutation-registry'
|
|
10
|
+
export { createClientEngine, resolveClientAuthData } from './client'
|
|
12
11
|
export {
|
|
13
12
|
ClientEngine,
|
|
14
13
|
__CONNECTED_CLOCK_META_KEY,
|
|
15
|
-
|
|
14
|
+
__LOGOUT_BEHAVIOR_PREF_KEY,
|
|
16
15
|
settledRejection,
|
|
17
16
|
isFalsyQuery,
|
|
18
17
|
normalizeQuerySurface,
|
|
@@ -25,13 +24,14 @@ export {
|
|
|
25
24
|
splitClientStatements,
|
|
26
25
|
type ClientStatementKind,
|
|
27
26
|
} from './migrate'
|
|
28
|
-
export { asClientMutation, validateMutationArgs } from './mutations'
|
|
29
27
|
export { extractWriteTable } from './port'
|
|
30
28
|
export {
|
|
31
29
|
applyBundledMigrations,
|
|
32
30
|
createEngineTables,
|
|
33
31
|
dropConsumerTables,
|
|
34
32
|
readMeta,
|
|
33
|
+
readPref,
|
|
35
34
|
replayMigrations,
|
|
36
35
|
writeMeta,
|
|
36
|
+
writePref,
|
|
37
37
|
} from './replica'
|
package/src/migrate.ts
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
CREATE TABLE `__doync_prefs` (
|
|
2
|
+
`k` text PRIMARY KEY NOT NULL,
|
|
3
|
+
`v` blob
|
|
4
|
+
) WITHOUT ROWID;
|
|
5
|
+
--> statement-breakpoint
|
|
6
|
+
INSERT INTO `__doync_prefs` (`k`, `v`)
|
|
7
|
+
SELECT `k`, `v` FROM `__doync_meta` WHERE `k` = 'logout_behavior';
|
|
8
|
+
--> statement-breakpoint
|
|
9
|
+
DELETE FROM `__doync_meta` WHERE `k` = 'logout_behavior';
|