@spooky-sync/core 0.0.1-canary.334 → 0.0.1-canary.336
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/AGENTS.md +8 -2
- package/README.md +4 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +140 -25
- package/package.json +3 -3
- package/skills/sp00ky-core/SKILL.md +6 -1
- package/src/mutation/ingest-outage.integration.test.ts +400 -0
- package/src/mutation/push.saga.test.ts +74 -14
- package/src/mutation/push.saga.ts +25 -5
- package/src/mutation/rows.test.ts +49 -2
- package/src/mutation/rows.ts +93 -21
- package/src/query/session-guard.integration.test.ts +3 -1
- package/src/state/client-state.ts +8 -0
- package/src/state/reducers.test.ts +3 -0
- package/src/state/reducers.ts +9 -0
- package/src/utils/error-classification.test.ts +33 -0
- package/src/utils/error-classification.ts +27 -0
package/AGENTS.md
CHANGED
|
@@ -27,7 +27,13 @@ Sp00kyClient<S> ── facade: every method = one saga run, selector or subscri
|
|
|
27
27
|
|
|
28
28
|
Local writes are applied optimistically (row + outbox entry in one local
|
|
29
29
|
transaction), rendered through the outbox overlay, and drained to the remote in
|
|
30
|
-
batches.
|
|
30
|
+
FIFO batches. Each statement of a batch commits on its own, but the server stops
|
|
31
|
+
the batch at its first failed statement (`remoteBatch` chains them), so no write
|
|
32
|
+
reaches the server ahead of an earlier one that failed. A batch whose answer is
|
|
33
|
+
lost (it threw a transport error, or its rows were loaded from the store after a
|
|
34
|
+
restart) is in doubt (`OutboxItem.inDoubt`): re-sent, an in-doubt CREATE checks
|
|
35
|
+
the row already on the server and is a no-op when it holds what the CREATE
|
|
36
|
+
writes. Server membership arrives through one batched edge read (registration,
|
|
31
37
|
poll, LIVE dirt) and bodies are fetched once across all queries. Rows are always
|
|
32
38
|
re-materialized from state, never patched in place.
|
|
33
39
|
|
|
@@ -59,7 +65,7 @@ re-materialized from state, never patched in place.
|
|
|
59
65
|
- **A never-resolved query paints only vouched rows.** Before the server answers (`cold`), the local predicate scan shows a cached body only if some `_00_view` row names it (as a member or subquery child) or a write for it is in flight. A body no view names is an orphan (deleted upstream while nothing watched it): it stays hidden, and the GC deletes it 30 s after boot and then hourly. A `_00_view` row no query has resolved for 14 days is retired by the same sweep, so that query paints cold again.
|
|
60
66
|
- **`database.store` defaults to `'memory'`.** `'indexeddb'` keeps one IndexedDB database per user bucket (`sp00ky-<bucket>`) and needs `@surrealdb/wasm` >= 3.1.0: the 3.0.x engine opened the store but failed every write (`An IndexedDB error occured: idb error`), so each session fell back to memory. 3.1.0 ships the engine as `@surrealdb/wasm-native`; a Vite app lists it in `optimizeDeps.exclude` next to `@surrealdb/wasm`. A store that still cannot open after a drop runs the session in memory (error log `falling back to in-memory`). Under `localEngine: 'sqlite'`, `'indexeddb'` means OPFS.
|
|
61
67
|
- **Boot owns the first identity.** The facade subscribes to auth with `immediate: false`: boot opens the bucket the hint names and `auth.restoreSession` announces the user. `LocalDatabaseService.switchStore` also waits for an in-flight `connect`, and lets statements already past the gate finish before it closes a client: the 3.1 engine streams results from its worker, and a statement whose client closed under it stayed pending until its 30 s deadline with every later local statement queued behind it.
|
|
62
|
-
- **A rejected write is not lost.** Application errors roll the local row back (create/update/delete) and move the mutation to `_00_failed_mutations`; the tray API above lists, retries (as a fresh write) or discards it. Network errors keep the row queued.
|
|
68
|
+
- **A rejected write is not lost.** Application errors roll the local row back (create/update/delete) and move the mutation to `_00_failed_mutations`; the tray API above lists, retries (as a fresh write) or discards it. Network errors keep the row queued, and so does a write SurrealDB refused only because sp00ky's own `_00_` ingest event could not post to the scheduler/SSP (refused, past its TIMEOUT, DNS, 5xx/408/429; `SP00KY_EVENT_POST_FAILED` in `utils/error-classification.ts`).
|
|
63
69
|
|
|
64
70
|
## Module map (under `src/`)
|
|
65
71
|
|
package/README.md
CHANGED
|
@@ -49,7 +49,10 @@ back.
|
|
|
49
49
|
acked (kept in the overlay until membership names them), rejected rows are
|
|
50
50
|
rolled back and moved to `_00_failed_mutations` in the same transaction that
|
|
51
51
|
deletes the pending row, a transport cut leaves the tail queued behind a
|
|
52
|
-
backoff.
|
|
52
|
+
backoff. A push whose answer never came (timeout, dropped socket, a session
|
|
53
|
+
that died mid-push) may have committed, so its rows are in doubt: re-sent, an
|
|
54
|
+
in-doubt CREATE that finds a matching row of its own on the server is a no-op
|
|
55
|
+
instead of an "already exists" rollback. The tray is exposed as `failedMutationCount`,
|
|
53
56
|
`subscribeToFailedMutations`, `listFailedMutations`, `retryFailedMutation`
|
|
54
57
|
and `discardFailedMutation`.
|
|
55
58
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1339,6 +1339,14 @@ interface OutboxItem {
|
|
|
1339
1339
|
readonly status: OutboxStatus;
|
|
1340
1340
|
readonly ackedAt: number | null;
|
|
1341
1341
|
readonly attempts: number;
|
|
1342
|
+
/**
|
|
1343
|
+
* A request carrying this row may have committed without the answer
|
|
1344
|
+
* reaching this client: it threw a transport error, or the row was loaded
|
|
1345
|
+
* from the store, so a session that died mid-push may have sent it. Its
|
|
1346
|
+
* CREATE then accepts the row already on the server when that row holds
|
|
1347
|
+
* what it would write (see `remoteBatch`). Absent means false.
|
|
1348
|
+
*/
|
|
1349
|
+
readonly inDoubt?: boolean;
|
|
1342
1350
|
}
|
|
1343
1351
|
type TabRole = 'solo' | 'leader' | 'follower';
|
|
1344
1352
|
/** A debounced update accumulating locally until its flush writes the outbox row. */
|
package/dist/index.js
CHANGED
|
@@ -546,8 +546,32 @@ const NETWORK_ERROR_PATTERNS = [
|
|
|
546
546
|
"specify a namespace",
|
|
547
547
|
"specify a database"
|
|
548
548
|
];
|
|
549
|
+
/**
|
|
550
|
+
* A write SurrealDB refused because sp00ky's OWN ingest post failed: every
|
|
551
|
+
* synced table's `_00_<table>_mutation` / `_00_<table>_delete` event posts to
|
|
552
|
+
* the scheduler or SSP inside the write's transaction (`sp00ky.rs::ingest_post`),
|
|
553
|
+
* so a scheduler that is down, slow past the event TIMEOUT, or answering 5xx
|
|
554
|
+
* aborts the write. That is an outage, not the mutation's fault, and it clears
|
|
555
|
+
* on its own; classified `application` it was rolled back and discarded, so
|
|
556
|
+
* every write queued during a scheduler restart or a cold SSP was lost.
|
|
557
|
+
*
|
|
558
|
+
* Wording on SurrealDB 3.1.5 and 3.3.4 (the event name may be nested inside an
|
|
559
|
+
* app event that wrote to a synced table):
|
|
560
|
+
* "Error while processing event _00_item_mutation: There was an error
|
|
561
|
+
* processing a remote HTTP request: error sending request for url (...)"
|
|
562
|
+
* (refused connection or TIMEOUT), "...: 503 Service Unavailable" (5xx),
|
|
563
|
+
* "Error while processing event _00_item_mutation: failed to lookup address
|
|
564
|
+
* information: ..." (DNS).
|
|
565
|
+
*
|
|
566
|
+
* Only `_00_` events count: an app's own event posting to its own endpoint
|
|
567
|
+
* stays `application`, as does a 4xx from `/ingest` other than 408/429 (a
|
|
568
|
+
* malformed or oversized payload, a secret mismatch), which no retry fixes and
|
|
569
|
+
* which would otherwise hold the outbox's serial lane forever.
|
|
570
|
+
*/
|
|
571
|
+
const SP00KY_EVENT_POST_FAILED = /error while processing event _00_[^:\s]*: (?:there was an error processing a remote http request: (?:error sending request|5\d\d\b|408\b|429\b)|failed to lookup address information)/;
|
|
549
572
|
function classifySyncError(error) {
|
|
550
573
|
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
574
|
+
if (SP00KY_EVENT_POST_FAILED.test(message)) return "network";
|
|
551
575
|
for (const pattern of NETWORK_ERROR_PATTERNS) if (message.includes(pattern)) return "network";
|
|
552
576
|
return "application";
|
|
553
577
|
}
|
|
@@ -1382,6 +1406,11 @@ function hydrateRowData(row, columns) {
|
|
|
1382
1406
|
return row;
|
|
1383
1407
|
}
|
|
1384
1408
|
}
|
|
1409
|
+
/**
|
|
1410
|
+
* A row read back from the store at boot, bucket switch or leader takeover.
|
|
1411
|
+
* The session that queued it may have sent it and died before the answer (a
|
|
1412
|
+
* closed tab, a killed app, a lost leader), so its outcome is in doubt.
|
|
1413
|
+
*/
|
|
1385
1414
|
const toOutboxItem = (row) => ({
|
|
1386
1415
|
id: row.id,
|
|
1387
1416
|
type: row.mutationType,
|
|
@@ -1389,7 +1418,8 @@ const toOutboxItem = (row) => ({
|
|
|
1389
1418
|
table: row.tableName,
|
|
1390
1419
|
status: "pending",
|
|
1391
1420
|
ackedAt: null,
|
|
1392
|
-
attempts: 0
|
|
1421
|
+
attempts: 0,
|
|
1422
|
+
inDoubt: true
|
|
1393
1423
|
});
|
|
1394
1424
|
/** The `_00_pending_writes` id for a minted mutation id (same suffix). */
|
|
1395
1425
|
const pendingWriteId = (mutationId) => `${PENDING_WRITES_TABLE}:${mutationId.includes(":") ? mutationId.slice(mutationId.indexOf(":") + 1) : mutationId}`;
|
|
@@ -1526,28 +1556,86 @@ function planDeferredOutboxRowTx(input) {
|
|
|
1526
1556
|
};
|
|
1527
1557
|
}
|
|
1528
1558
|
/**
|
|
1529
|
-
*
|
|
1530
|
-
*
|
|
1559
|
+
* What a statement of a `remoteBatch` throws instead of running, because an
|
|
1560
|
+
* earlier statement in the same request failed.
|
|
1561
|
+
*/
|
|
1562
|
+
const BATCH_SKIPPED = "sp00ky: not run, an earlier statement in this batch failed";
|
|
1563
|
+
/** A CRDT edit-log row (`<table>__crdt_log`): written once under a fresh id, never changed. */
|
|
1564
|
+
const isLogTable = (table) => table.endsWith("__crdt_log");
|
|
1565
|
+
/**
|
|
1566
|
+
* Which fields of the row already on the server must equal the payload of the
|
|
1567
|
+
* CREATE at `i` for that row to count as this CREATE's own earlier commit, or
|
|
1568
|
+
* `null` when the CREATE must not tolerate an existing row at all.
|
|
1569
|
+
*
|
|
1570
|
+
* Only a CREATE whose outcome is in doubt tolerates one: elsewhere "already
|
|
1571
|
+
* exists" means someone else holds the id, a rejection as before. The check
|
|
1572
|
+
* is every payload field except those a later in-doubt row of the same
|
|
1573
|
+
* record, which may have committed in the same lost round, could have changed
|
|
1574
|
+
* since; those rows run again after it and set them anyway. A later in-doubt
|
|
1575
|
+
* DELETE or CREATE of the record may have replaced the row, so then existence
|
|
1576
|
+
* is all that is checked. Rows that are not in doubt never ran.
|
|
1577
|
+
*
|
|
1578
|
+
* A CRDT log row's id is fresh and the row immutable, so its existence is
|
|
1579
|
+
* proof enough, in doubt or not.
|
|
1580
|
+
*/
|
|
1581
|
+
function matchFields(rows, i, inDoubt) {
|
|
1582
|
+
const row = rows[i];
|
|
1583
|
+
if (isLogTable(row.tableName)) return [];
|
|
1584
|
+
if (!inDoubt.has(row.id)) return null;
|
|
1585
|
+
const changed = /* @__PURE__ */ new Set();
|
|
1586
|
+
for (const later of rows.slice(i + 1)) {
|
|
1587
|
+
if (later.recordId !== row.recordId || !inDoubt.has(later.id)) continue;
|
|
1588
|
+
if (later.mutationType !== "update") return [];
|
|
1589
|
+
for (const key of Object.keys(later.data ?? {})) changed.add(key);
|
|
1590
|
+
}
|
|
1591
|
+
return Object.keys(row.data ?? {}).filter((key) => !changed.has(key));
|
|
1592
|
+
}
|
|
1593
|
+
function remoteStatement(rows, i, vars, inDoubt) {
|
|
1594
|
+
const row = rows[i];
|
|
1595
|
+
vars[`id${i}`] = parseRecordIdString(row.recordId);
|
|
1596
|
+
switch (row.mutationType) {
|
|
1597
|
+
case "create": {
|
|
1598
|
+
const data = row.data ?? {};
|
|
1599
|
+
const create = `CREATE ONLY $id${i} SET ${Object.keys(data).map((key) => {
|
|
1600
|
+
vars[`d${i}_${key}`] = data[key];
|
|
1601
|
+
return `${key} = $d${i}_${key}`;
|
|
1602
|
+
}).join(", ")}`;
|
|
1603
|
+
const match = matchFields(rows, i, inDoubt);
|
|
1604
|
+
if (!match) return create;
|
|
1605
|
+
return `LET $was${i} = (SELECT * FROM ONLY $id${i}); IF ${[`($was${i} = NONE)`, ...match.map((key) => `($was${i}.${key} != $d${i}_${key})`)].join(" OR ")} { ${create} }`;
|
|
1606
|
+
}
|
|
1607
|
+
case "update":
|
|
1608
|
+
vars[`data${i}`] = row.data ?? {};
|
|
1609
|
+
return `UPDATE $id${i} MERGE $data${i}`;
|
|
1610
|
+
case "delete": return `DELETE $id${i}`;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Many outbox rows as ONE multi-statement request. Statement i answers row i
|
|
1615
|
+
* and is its own transaction on the server, as before, but the statements are
|
|
1616
|
+
* chained: statement i binds `$ok<i>` only when it commits, and statement i+1
|
|
1617
|
+
* throws `BATCH_SKIPPED` without touching anything unless `$ok<i>` is bound
|
|
1618
|
+
* (a LET lives for one request only). So the server stops at the first
|
|
1619
|
+
* failure: everything before it is committed, nothing after it ran.
|
|
1620
|
+
*
|
|
1621
|
+
* Without the chain, statements after a failed one ran anyway. A retryable
|
|
1622
|
+
* failure at i left the rows after i queued although they had committed; the
|
|
1623
|
+
* re-sent CREATE then read "already exists", was taken for a rejection and
|
|
1624
|
+
* rolled back locally while the server kept the row. Acking them instead
|
|
1625
|
+
* would have let a later write land before an earlier one it may depend on.
|
|
1626
|
+
*
|
|
1627
|
+
* The chain cannot help when the answer itself is lost: the request may have
|
|
1628
|
+
* committed any prefix of the batch. Rows named in `inDoubt` (mutation ids)
|
|
1629
|
+
* are such rows. An UPDATE MERGE or a DELETE is safe to run twice, a CREATE
|
|
1630
|
+
* is not, so an in-doubt CREATE checks the row already there first (see
|
|
1631
|
+
* `matchFields`) and is a no-op when it is its own: the batch goes on in
|
|
1632
|
+
* order, with no "already exists" to cut it.
|
|
1531
1633
|
*/
|
|
1532
|
-
function remoteBatch(rows) {
|
|
1634
|
+
function remoteBatch(rows, inDoubt = /* @__PURE__ */ new Set()) {
|
|
1533
1635
|
const vars = {};
|
|
1534
1636
|
return {
|
|
1535
|
-
sql: rows.map((
|
|
1536
|
-
|
|
1537
|
-
vars[`id${i}`] = rid;
|
|
1538
|
-
switch (row.mutationType) {
|
|
1539
|
-
case "create": {
|
|
1540
|
-
const data = row.data ?? {};
|
|
1541
|
-
return `CREATE ONLY $id${i} SET ${Object.keys(data).map((key) => {
|
|
1542
|
-
vars[`d${i}_${key}`] = data[key];
|
|
1543
|
-
return `${key} = $d${i}_${key}`;
|
|
1544
|
-
}).join(", ")}`;
|
|
1545
|
-
}
|
|
1546
|
-
case "update":
|
|
1547
|
-
vars[`data${i}`] = row.data ?? {};
|
|
1548
|
-
return `UPDATE $id${i} MERGE $data${i}`;
|
|
1549
|
-
case "delete": return `DELETE $id${i}`;
|
|
1550
|
-
}
|
|
1637
|
+
sql: rows.map((_, i) => {
|
|
1638
|
+
return `LET $ok${i} = { ${i === 0 ? "" : `IF !$ok${i - 1} { THROW "${BATCH_SKIPPED}" }; `}${remoteStatement(rows, i, vars, inDoubt)}; true }`;
|
|
1551
1639
|
}).join(";\n"),
|
|
1552
1640
|
vars
|
|
1553
1641
|
};
|
|
@@ -2198,8 +2286,8 @@ function trimHistory(history, cap) {
|
|
|
2198
2286
|
|
|
2199
2287
|
//#endregion
|
|
2200
2288
|
//#region src/modules/devtools/index.ts
|
|
2201
|
-
const CORE_VERSION = "0.0.1-canary.
|
|
2202
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
2289
|
+
const CORE_VERSION = "0.0.1-canary.336";
|
|
2290
|
+
const WASM_VERSION = "0.0.1-canary.336";
|
|
2203
2291
|
const SURREAL_VERSION = "3.1.0";
|
|
2204
2292
|
var DevToolsService = class DevToolsService {
|
|
2205
2293
|
version = CORE_VERSION;
|
|
@@ -3458,6 +3546,18 @@ const outboxBumpAttempts = (id) => (s) => {
|
|
|
3458
3546
|
outbox
|
|
3459
3547
|
};
|
|
3460
3548
|
};
|
|
3549
|
+
/** Mark items whose last push may have committed although no answer came back. */
|
|
3550
|
+
const outboxMarkInDoubt = (ids) => (s) => {
|
|
3551
|
+
const marked = new Set(ids);
|
|
3552
|
+
if (!s.outbox.some((i) => marked.has(i.id) && !i.inDoubt)) return s;
|
|
3553
|
+
return {
|
|
3554
|
+
...s,
|
|
3555
|
+
outbox: s.outbox.map((i) => marked.has(i.id) && !i.inDoubt ? {
|
|
3556
|
+
...i,
|
|
3557
|
+
inDoubt: true
|
|
3558
|
+
} : i)
|
|
3559
|
+
};
|
|
3560
|
+
};
|
|
3461
3561
|
/** Drop acked items older than `graceMs` (membership never named them). */
|
|
3462
3562
|
const outboxPruneAcked = (now, graceMs) => (s) => {
|
|
3463
3563
|
const stale = s.outbox.filter((i) => i.status === "acked" && i.ackedAt !== null && now - i.ackedAt >= graceMs);
|
|
@@ -5809,6 +5909,8 @@ const ERR = (r) => r && r.status === "ERR" ? r.error : null;
|
|
|
5809
5909
|
* A CRDT edit-log row (`<table>__crdt_log`) is written once and never changes,
|
|
5810
5910
|
* so a CREATE retried after its first attempt committed (and only the answer
|
|
5811
5911
|
* was lost) finds it already there: that is a success, not a rejection.
|
|
5912
|
+
* `remoteBatch` already makes such a CREATE a no-op on the server; this covers
|
|
5913
|
+
* a row the user may create but not read back, where that check cannot see it.
|
|
5812
5914
|
*/
|
|
5813
5915
|
const isRepeatedLogCreate = (row, err) => row.mutationType === "create" && row.tableName.endsWith("__crdt_log") && /already exists/i.test(err);
|
|
5814
5916
|
/**
|
|
@@ -5816,6 +5918,17 @@ const isRepeatedLogCreate = (row, err) => row.mutationType === "create" && row.t
|
|
|
5816
5918
|
* per request, per-statement outcome. Accepted rows are acked (they stay in
|
|
5817
5919
|
* the overlay until membership names them), rejected rows are rolled back,
|
|
5818
5920
|
* a transport failure leaves the unsent tail queued behind a backoff timer.
|
|
5921
|
+
* The server stops a batch at its first failed statement (`remoteBatch`), so
|
|
5922
|
+
* the rows after a rejection are sent again at once, and a re-sent row behind
|
|
5923
|
+
* a retryable failure never ran before, so it cannot read "already exists".
|
|
5924
|
+
*
|
|
5925
|
+
* A request that throws a transport error (the push timeout, a dropped
|
|
5926
|
+
* socket) may still have committed on the server, and so may one sent by a
|
|
5927
|
+
* session that died before its answer. Those rows are marked in doubt
|
|
5928
|
+
* (`OutboxItem.inDoubt`, every row loaded from the store is), and their
|
|
5929
|
+
* re-sent CREATE accepts the row already on the server when it holds what the
|
|
5930
|
+
* CREATE would write, instead of failing "already exists" and rolling back a
|
|
5931
|
+
* write the server kept.
|
|
5819
5932
|
*/
|
|
5820
5933
|
function* drain(env) {
|
|
5821
5934
|
for (;;) {
|
|
@@ -5837,7 +5950,7 @@ function* drain(env) {
|
|
|
5837
5950
|
else yield fx.state.update(outboxRemove(item.id));
|
|
5838
5951
|
}
|
|
5839
5952
|
if (rows.length === 0) continue;
|
|
5840
|
-
const req = remoteBatch(rows);
|
|
5953
|
+
const req = remoteBatch(rows, new Set(batch.filter((i) => i.inDoubt).map((i) => i.id)));
|
|
5841
5954
|
let results;
|
|
5842
5955
|
try {
|
|
5843
5956
|
const raw = yield fx.remote.query(req.sql, req.vars, env.pushTimeoutMs);
|
|
@@ -5856,7 +5969,7 @@ function* drain(env) {
|
|
|
5856
5969
|
continue;
|
|
5857
5970
|
}
|
|
5858
5971
|
const attempts = batch.find((i) => i.id === rows[0].id).attempts;
|
|
5859
|
-
yield fx.state.update(outboxBumpAttempts(rows[0].id));
|
|
5972
|
+
yield fx.state.update(compose(outboxMarkInDoubt(rows.map((row) => row.id)), outboxBumpAttempts(rows[0].id)));
|
|
5860
5973
|
yield fx.dispatch({
|
|
5861
5974
|
type: "SyncOutcome",
|
|
5862
5975
|
ok: false,
|
|
@@ -5871,6 +5984,7 @@ function* drain(env) {
|
|
|
5871
5984
|
const row = rows[i];
|
|
5872
5985
|
const r = results[i];
|
|
5873
5986
|
if (!r) {
|
|
5987
|
+
yield fx.state.update(outboxMarkInDoubt(rows.slice(i).map((unanswered) => unanswered.id)));
|
|
5874
5988
|
cutAt = i;
|
|
5875
5989
|
break;
|
|
5876
5990
|
}
|
|
@@ -5908,6 +6022,7 @@ function* drain(env) {
|
|
|
5908
6022
|
});
|
|
5909
6023
|
continue;
|
|
5910
6024
|
}
|
|
6025
|
+
if (err.includes(BATCH_SKIPPED)) break;
|
|
5911
6026
|
if (classifySyncError(new Error(err)) === "network") {
|
|
5912
6027
|
cutAt = i;
|
|
5913
6028
|
break;
|
|
@@ -15774,7 +15889,7 @@ function createTabsCoordinator(config, s, host) {
|
|
|
15774
15889
|
return new TabsCoordinator({
|
|
15775
15890
|
tabId: s.tabId,
|
|
15776
15891
|
fingerprint: computeTabsFingerprint({
|
|
15777
|
-
coreVersion: "0.0.1-canary.
|
|
15892
|
+
coreVersion: "0.0.1-canary.336",
|
|
15778
15893
|
schemaHash: hash53(config.schemaSurql),
|
|
15779
15894
|
endpoint: config.database.endpoint ?? "",
|
|
15780
15895
|
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.336",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@spooky-sync/query-builder": "0.0.1-canary.
|
|
79
|
-
"@spooky-sync/ssp-wasm": "0.0.1-canary.
|
|
78
|
+
"@spooky-sync/query-builder": "0.0.1-canary.336",
|
|
79
|
+
"@spooky-sync/ssp-wasm": "0.0.1-canary.336",
|
|
80
80
|
"@sqlite.org/sqlite-wasm": "3.53.0-build1",
|
|
81
81
|
"@surrealdb/wasm": "^3.1.0",
|
|
82
82
|
"@surrealdb/wasm-native": "^3.3.4",
|
|
@@ -129,7 +129,12 @@ Use `debounced` option for frequent updates (e.g., live typing):
|
|
|
129
129
|
|
|
130
130
|
A push the server rejects (permission, validation) is rolled back locally
|
|
131
131
|
(create, update and delete) and moved to the `_00_failed_mutations` tray in the
|
|
132
|
-
same local transaction. Network errors never do this; they retry
|
|
132
|
+
same local transaction. Network errors never do this; they retry, and so does a
|
|
133
|
+
write the server refused only because sp00ky's own ingest post to the scheduler
|
|
134
|
+
failed (down, past its timeout, or answering 5xx). A create whose push answer
|
|
135
|
+
was lost (timeout, dropped socket, a tab or app closed mid-push) is not rolled
|
|
136
|
+
back when its retry finds the row it wrote already on the server; a row there
|
|
137
|
+
with other content is still "already exists". Inspect and
|
|
133
138
|
act on the tray with `db.failedMutationCount`, `db.subscribeToFailedMutations(cb)`,
|
|
134
139
|
`db.listFailedMutations()`, `db.retryFailedMutation(id)` (a fresh optimistic
|
|
135
140
|
write) and `db.discardFailedMutation(id)`.
|