@bayonai/offline 0.8.0 → 0.10.0

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +107 -16
  3. package/RECOVERY.md +64 -0
  4. package/dist/browserFallbackRecovery.d.ts +10 -9
  5. package/dist/browserFallbackRecovery.js +100 -28
  6. package/dist/browserRecovery.d.ts +2 -2
  7. package/dist/durableCommandStore.d.ts +1 -1
  8. package/dist/idbDriver.d.ts +3 -1
  9. package/dist/idbDriver.js +29 -14
  10. package/dist/index.d.ts +10 -7
  11. package/dist/index.js +3 -0
  12. package/dist/pendingWriteSupersession.d.ts +11 -0
  13. package/dist/pendingWriteSupersession.js +25 -0
  14. package/dist/pendingWrites.d.ts +6 -2
  15. package/dist/pendingWrites.js +45 -11
  16. package/dist/readBoundary.d.ts +13 -0
  17. package/dist/readBoundary.js +23 -0
  18. package/dist/readCache.d.ts +26 -5
  19. package/dist/readCache.js +113 -19
  20. package/dist/recoveryPolicy.d.ts +20 -0
  21. package/dist/recoveryPolicy.js +73 -0
  22. package/dist-cjs/browserCoordination.d.ts +23 -0
  23. package/dist-cjs/browserFallbackRecovery.d.ts +33 -0
  24. package/dist-cjs/browserFallbackRecovery.js +100 -28
  25. package/dist-cjs/browserRecovery.d.ts +42 -0
  26. package/dist-cjs/durableCommandStore.d.ts +49 -0
  27. package/dist-cjs/idbDriver.d.ts +28 -0
  28. package/dist-cjs/idbDriver.js +29 -14
  29. package/dist-cjs/index.d.ts +10 -0
  30. package/dist-cjs/index.js +3 -0
  31. package/dist-cjs/pendingWriteSupersession.d.ts +11 -0
  32. package/dist-cjs/pendingWriteSupersession.js +28 -0
  33. package/dist-cjs/pendingWrites.d.ts +179 -0
  34. package/dist-cjs/pendingWrites.js +45 -11
  35. package/dist-cjs/readBoundary.d.ts +13 -0
  36. package/dist-cjs/readBoundary.js +26 -0
  37. package/dist-cjs/readCache.d.ts +112 -0
  38. package/dist-cjs/readCache.js +113 -19
  39. package/dist-cjs/recoveryPolicy.d.ts +20 -0
  40. package/dist-cjs/recoveryPolicy.js +78 -0
  41. package/package.json +16 -7
@@ -0,0 +1,33 @@
1
+ export declare const defaultOfflineFallbackRetryEvent = "app:network-restored";
2
+ export declare const defaultOfflineFallbackPullThreshold = 72;
3
+ export type BrowserOfflineFallbackRecoveryEnvironment = {
4
+ addEventListener: (type: string, listener: (event: unknown) => void) => void;
5
+ getScrollY: () => number;
6
+ reload: () => void;
7
+ removeEventListener: (type: string, listener: (event: unknown) => void) => void;
8
+ };
9
+ export type OfflineFallbackRetryControl = {
10
+ disabled: boolean;
11
+ textContent: string | null;
12
+ };
13
+ export type BrowserOfflineFallbackRecovery = {
14
+ /** Requests one safe refresh; unsafe or failed proof is rechecked automatically. */
15
+ refresh: () => void;
16
+ /** Compatibility for hosts that label navigation as Reload. */
17
+ reload: () => void;
18
+ /** @deprecated Internal compatibility alias; never a user retry instruction. */
19
+ retry: () => void;
20
+ start: () => void;
21
+ stop: () => void;
22
+ };
23
+ export type BrowserOfflineFallbackRecoveryOptions = {
24
+ /** Optional legacy UI; new hosts use pull-down without a refresh button. */
25
+ control?: OfflineFallbackRetryControl;
26
+ canReload?: () => boolean | Promise<boolean>;
27
+ environment?: BrowserOfflineFallbackRecoveryEnvironment;
28
+ retryEvent?: string;
29
+ retryingText?: string;
30
+ pullThreshold?: number | null;
31
+ };
32
+ /** Owns pull/reconnect recovery, including safe asynchronous durability proof. */
33
+ export declare function createBrowserOfflineFallbackRecovery({ control, canReload, environment, pullThreshold, retryEvent, retryingText, }: BrowserOfflineFallbackRecoveryOptions): BrowserOfflineFallbackRecovery;
@@ -2,70 +2,142 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.defaultOfflineFallbackPullThreshold = exports.defaultOfflineFallbackRetryEvent = void 0;
4
4
  exports.createBrowserOfflineFallbackRecovery = createBrowserOfflineFallbackRecovery;
5
+ const recoveryPolicy_1 = require("./recoveryPolicy");
6
+ const browserRecovery_1 = require("./browserRecovery");
5
7
  exports.defaultOfflineFallbackRetryEvent = "app:network-restored";
6
8
  exports.defaultOfflineFallbackPullThreshold = 72;
7
- /**
8
- * Owns generic recovery mechanics for a static offline fallback. Hosts retain
9
- * all markup, copy, styling, and the choice of a native reconnect event.
10
- */
11
- function createBrowserOfflineFallbackRecovery({ control, environment = createDefaultBrowserOfflineFallbackRecoveryEnvironment(), pullThreshold = exports.defaultOfflineFallbackPullThreshold, retryEvent = exports.defaultOfflineFallbackRetryEvent, retryingText = "Reconnecting…", }) {
9
+ /** Owns pull/reconnect recovery, including safe asynchronous durability proof. */
10
+ function createBrowserOfflineFallbackRecovery({ control, canReload, environment = createDefaultBrowserOfflineFallbackRecoveryEnvironment(), pullThreshold = exports.defaultOfflineFallbackPullThreshold, retryEvent = exports.defaultOfflineFallbackRetryEvent, retryingText = recoveryPolicy_1.offlineRecoveryCopy.reloading, }) {
11
+ if (control)
12
+ control.textContent = recoveryPolicy_1.offlineRecoveryCopy.reload;
12
13
  let active = false;
13
- let retrying = false;
14
+ let navigating = false;
15
+ let checking = false;
16
+ let requested = false;
17
+ let generation = 0;
18
+ let timer;
19
+ let delay = browserRecovery_1.initialPendingWriteRetryDelayMs;
14
20
  let pullStartY = null;
15
- const touchThreshold = pullThreshold === null ? Number.POSITIVE_INFINITY : pullThreshold;
16
- function retry() {
17
- if (retrying)
21
+ const touchThreshold = pullThreshold === null ? Infinity : pullThreshold;
22
+ function clearTimer() {
23
+ if (timer !== undefined)
24
+ clearTimeout(timer);
25
+ timer = undefined;
26
+ }
27
+ function schedule() {
28
+ if (!active || !requested || timer !== undefined)
29
+ return;
30
+ timer = setTimeout(() => {
31
+ timer = undefined;
32
+ attempt();
33
+ }, delay);
34
+ delay = Math.min(delay * 2, browserRecovery_1.maxPendingWriteRetryDelayMs);
35
+ }
36
+ function failed(error) {
37
+ console.error("[Bayon offline] Offline fallback refresh could not complete; local work retained.", { error });
38
+ navigating = false;
39
+ if (control) {
40
+ control.disabled = false;
41
+ control.textContent = recoveryPolicy_1.offlineRecoveryCopy.reload;
42
+ }
43
+ schedule();
44
+ }
45
+ function complete(safe, currentGeneration) {
46
+ if (currentGeneration !== generation)
47
+ return;
48
+ checking = false;
49
+ if (!safe) {
50
+ schedule();
18
51
  return;
19
- retrying = true;
20
- control.disabled = true;
21
- control.textContent = retryingText;
52
+ }
53
+ clearTimer();
54
+ navigating = true;
55
+ if (control) {
56
+ control.disabled = true;
57
+ control.textContent = retryingText;
58
+ }
22
59
  try {
23
60
  environment.reload();
61
+ requested = false;
24
62
  }
25
63
  catch (error) {
26
- // A failed navigation must leave the recovery action visible and usable.
27
- console.error("[Bayon offline] Offline fallback reload failed.", {
28
- error,
29
- });
30
- retrying = false;
31
- control.disabled = false;
64
+ failed(error);
32
65
  }
33
66
  }
67
+ function attempt() {
68
+ if (!requested || checking || navigating)
69
+ return;
70
+ checking = true;
71
+ const currentGeneration = generation;
72
+ try {
73
+ const proof = canReload ? canReload() : true;
74
+ if (typeof proof === "boolean")
75
+ complete(proof, currentGeneration);
76
+ else
77
+ void proof.then((safe) => complete(safe, currentGeneration), (error) => {
78
+ if (currentGeneration !== generation)
79
+ return;
80
+ checking = false;
81
+ failed(error);
82
+ });
83
+ }
84
+ catch (error) {
85
+ checking = false;
86
+ failed(error);
87
+ }
88
+ }
89
+ function refresh() {
90
+ requested = true;
91
+ clearTimer();
92
+ delay = browserRecovery_1.initialPendingWriteRetryDelayMs;
93
+ attempt();
94
+ }
34
95
  function handleTouchStart(event) {
35
- const touchY = getSingleTouchY(event, "touches");
36
- pullStartY = environment.getScrollY() === 0 ? touchY : null;
96
+ pullStartY =
97
+ environment.getScrollY() === 0 ? getSingleTouchY(event, "touches") : null;
37
98
  }
38
99
  function handleTouchEnd(event) {
39
100
  const touchY = getSingleTouchY(event, "changedTouches");
40
101
  if (pullStartY !== null &&
41
102
  touchY !== null &&
42
- touchY - pullStartY >= touchThreshold) {
43
- retry();
44
- }
103
+ touchY - pullStartY >= touchThreshold)
104
+ refresh();
105
+ pullStartY = null;
106
+ }
107
+ function handleTouchCancel() {
45
108
  pullStartY = null;
46
109
  }
47
110
  return {
48
- retry,
111
+ refresh,
112
+ reload: refresh,
113
+ retry: refresh,
49
114
  start() {
50
115
  if (active)
51
116
  return;
52
117
  active = true;
53
- environment.addEventListener("online", retry);
54
- environment.addEventListener(retryEvent, retry);
118
+ environment.addEventListener("online", refresh);
119
+ environment.addEventListener(retryEvent, refresh);
55
120
  if (pullThreshold !== null) {
56
121
  environment.addEventListener("touchstart", handleTouchStart);
57
122
  environment.addEventListener("touchend", handleTouchEnd);
123
+ environment.addEventListener("touchcancel", handleTouchCancel);
58
124
  }
59
125
  },
60
126
  stop() {
127
+ // Fence a proof completing after unmount, including manual pre-start calls.
128
+ generation++;
129
+ requested = false;
130
+ checking = false;
131
+ clearTimer();
61
132
  if (!active)
62
133
  return;
63
134
  active = false;
64
- environment.removeEventListener("online", retry);
65
- environment.removeEventListener(retryEvent, retry);
135
+ environment.removeEventListener("online", refresh);
136
+ environment.removeEventListener(retryEvent, refresh);
66
137
  if (pullThreshold !== null) {
67
138
  environment.removeEventListener("touchstart", handleTouchStart);
68
139
  environment.removeEventListener("touchend", handleTouchEnd);
140
+ environment.removeEventListener("touchcancel", handleTouchCancel);
69
141
  }
70
142
  pullStartY = null;
71
143
  },
@@ -0,0 +1,42 @@
1
+ import { type PendingWriteRecord, type PendingWriteStore, type PendingWriteSync, type SyncPendingWritesResult } from "./pendingWrites.js";
2
+ import type { BrowserPendingWriteRecoveryCoordination } from "./browserCoordination.js";
3
+ export declare const initialPendingWriteRetryDelayMs = 2000;
4
+ export declare const maxPendingWriteRetryDelayMs = 60000;
5
+ export type BrowserPendingWriteRecoveryEnvironment = {
6
+ addEventListener: (type: "focus" | "offline" | "online", listener: () => void) => void;
7
+ clearTimeout: (timer: ReturnType<typeof setTimeout>) => void;
8
+ isOnline: () => boolean;
9
+ removeEventListener: (type: "focus" | "offline" | "online", listener: () => void) => void;
10
+ setTimeout: (callback: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
11
+ };
12
+ export type BrowserPendingWriteRecovery<Payload> = {
13
+ /** Signals that a caller has persisted a record outside the store subscription. */
14
+ notifyQueued: () => void;
15
+ /** Runs one immediate, silent recovery attempt when the browser is online. */
16
+ recoverNow: () => Promise<SyncPendingWritesResult | null>;
17
+ start: () => void;
18
+ stop: () => void;
19
+ };
20
+ type BrowserPendingWriteRecoveryOptions<Payload> = {
21
+ /** Optional BroadcastChannel/Web Locks coordination supplied by the package factory. */
22
+ coordination?: BrowserPendingWriteRecoveryCoordination;
23
+ environment?: BrowserPendingWriteRecoveryEnvironment;
24
+ initialRetryDelayMs?: number;
25
+ maxRetryDelayMs?: number;
26
+ /** Diagnostic callback only. Recovery never renders or presents product UI. */
27
+ onError?: (error: unknown) => void;
28
+ /** Diagnostic callback only. Recovery never renders or presents product UI. */
29
+ onResult?: (result: SyncPendingWritesResult) => void;
30
+ scopeKey?: string;
31
+ select?: (record: PendingWriteRecord<Payload>) => boolean;
32
+ store: PendingWriteStore<Payload>;
33
+ } & ({
34
+ /** Runs one complete host-owned recovery pass, such as through an ordered executor. */
35
+ recover: () => Promise<SyncPendingWritesResult>;
36
+ sync?: never;
37
+ } | {
38
+ recover?: never;
39
+ sync: PendingWriteSync<Payload>;
40
+ });
41
+ export declare function createBrowserPendingWriteRecovery<Payload>(options: BrowserPendingWriteRecoveryOptions<Payload>): BrowserPendingWriteRecovery<Payload>;
42
+ export {};
@@ -0,0 +1,49 @@
1
+ import { type PendingWriteRecord, type PendingWriteStore } from "./pendingWrites.js";
2
+ export type DurableCommandErrorCategory = "auth" | "conflict" | "connectivity" | "local-storage" | "permission" | "unexpected" | "validation";
3
+ export type DurableCommandOperation<Command> = {
4
+ command: Command;
5
+ error?: DurableCommandErrorCategory;
6
+ id: string;
7
+ status: "accepted" | "failed" | "syncing";
8
+ };
9
+ export type DurableCommandSnapshot<State, Command> = {
10
+ operations: DurableCommandOperation<Command>[];
11
+ state: State;
12
+ };
13
+ export type DurableCommandDispatch = {
14
+ accepted: Promise<void>;
15
+ operationId: string;
16
+ };
17
+ export type DurableCommandExecutionContext = {
18
+ /** Stable across retries so the remote command can be idempotent. */
19
+ operationId: string;
20
+ };
21
+ export type DurableCommandStore<State, Command, Scope> = {
22
+ cancel: (operationId: string) => Promise<boolean>;
23
+ close: () => void;
24
+ dispatch: (command: Command, options?: {
25
+ notBefore?: string;
26
+ }) => DurableCommandDispatch;
27
+ getSnapshot: () => DurableCommandSnapshot<State, Command>;
28
+ recover: () => Promise<void>;
29
+ refresh: (scope: Scope) => Promise<void>;
30
+ subscribe: (listener: () => void) => () => void;
31
+ };
32
+ /**
33
+ * Hosts provide domain projection and transport; this package only owns the
34
+ * durable command lifecycle. A command becomes visible immediately, but is
35
+ * never sent remotely until IndexedDB has accepted it.
36
+ */
37
+ export declare function createDurableCommandStore<State, Command, Scope, Result = unknown>({ canCancel, classifyError, createId, execute, initialState, load, now, project, reconcile, shouldRetry, store, }: {
38
+ canCancel?: (command: Command, now: string) => boolean;
39
+ classifyError?: (error: unknown) => DurableCommandErrorCategory;
40
+ createId?: () => string;
41
+ execute: (command: Command, context: DurableCommandExecutionContext) => Promise<Result>;
42
+ initialState: State;
43
+ load: (scope: Scope) => Promise<State>;
44
+ now?: () => string;
45
+ project: (state: State, commands: readonly PendingWriteRecord<Command>[]) => State;
46
+ reconcile?: (state: State, command: Command, result: Result) => State;
47
+ shouldRetry?: (category: DurableCommandErrorCategory, command: Command) => boolean;
48
+ store: PendingWriteStore<Command>;
49
+ }): DurableCommandStore<State, Command, Scope>;
@@ -0,0 +1,28 @@
1
+ export type BrowserKeyValueDriver = {
2
+ clear: () => Promise<void>;
3
+ close: () => Promise<void>;
4
+ delete: (key: string) => Promise<void>;
5
+ deleteIf: (key: string, predicate: KeyValuePredicate) => Promise<boolean>;
6
+ get: (key: string) => Promise<unknown | null>;
7
+ list: () => Promise<unknown[]>;
8
+ listEntries: () => Promise<Array<{
9
+ key: string;
10
+ value: unknown;
11
+ }>>;
12
+ set: (key: string, value: unknown) => Promise<void>;
13
+ setIf: (key: string, value: unknown, predicate: KeyValuePredicate) => Promise<boolean>;
14
+ };
15
+ export type KeyValuePredicate = (value: unknown | null) => boolean;
16
+ export type BrowserKeyValueDriverOptions = {
17
+ databaseName: string;
18
+ databaseVersion?: number;
19
+ indexedDB?: IDBFactory;
20
+ operationTimeoutMs: number;
21
+ /** Queue reads must throw; read caches may retain best-effort fallback. */
22
+ readFailureMode?: "fallback" | "throw";
23
+ storeName: string;
24
+ };
25
+ export declare class OfflineStorageError extends Error {
26
+ name: string;
27
+ }
28
+ export declare function createIdbKeyValueDriver({ databaseName, databaseVersion, indexedDB, operationTimeoutMs, readFailureMode, storeName, }: BrowserKeyValueDriverOptions): BrowserKeyValueDriver | null;
@@ -7,16 +7,28 @@ class OfflineStorageError extends Error {
7
7
  name = "OfflineStorageError";
8
8
  }
9
9
  exports.OfflineStorageError = OfflineStorageError;
10
- function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB = globalThis.indexedDB, operationTimeoutMs, storeName, }) {
10
+ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB = globalThis.indexedDB, operationTimeoutMs, readFailureMode = "fallback", storeName, }) {
11
11
  if (!indexedDB) {
12
12
  return null;
13
13
  }
14
- const databasePromise = openKeyValueDatabase({
15
- databaseName,
16
- databaseVersion,
17
- indexedDB,
18
- storeName,
19
- });
14
+ let databasePromise = null;
15
+ function getDatabase() {
16
+ if (!databasePromise) {
17
+ const attempt = openKeyValueDatabase({
18
+ databaseName,
19
+ databaseVersion,
20
+ indexedDB: indexedDB,
21
+ storeName,
22
+ });
23
+ databasePromise = attempt;
24
+ // A transient failed open must not poison every later recovery read.
25
+ void attempt.catch(() => {
26
+ databasePromise = null;
27
+ });
28
+ }
29
+ return databasePromise;
30
+ }
31
+ getDatabase();
20
32
  function runMutation(operationName, mutate) {
21
33
  return runTransactionalStorageOperation({
22
34
  failureMode: "throw",
@@ -24,7 +36,7 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
24
36
  operationName,
25
37
  storeName,
26
38
  operation: async (setTransaction, isCancelled) => {
27
- const database = await databasePromise;
39
+ const database = await getDatabase();
28
40
  if (isCancelled())
29
41
  return;
30
42
  const transaction = database.transaction(storeName, "readwrite");
@@ -45,7 +57,7 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
45
57
  operationName: "close database",
46
58
  storeName,
47
59
  operation: async () => {
48
- const database = await databasePromise;
60
+ const database = await getDatabase();
49
61
  database.close();
50
62
  },
51
63
  timeoutMs: operationTimeoutMs,
@@ -56,7 +68,7 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
56
68
  },
57
69
  async deleteIf(key, predicate) {
58
70
  return runConditionalStorageOperation({
59
- databasePromise,
71
+ databasePromise: getDatabase(),
60
72
  operationName: "conditionally delete key",
61
73
  storeName,
62
74
  timeoutMs: operationTimeoutMs,
@@ -71,11 +83,12 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
71
83
  },
72
84
  async get(key) {
73
85
  return runBrowserStorageOperation({
86
+ failureMode: readFailureMode,
74
87
  fallback: null,
75
88
  operationName: "read key",
76
89
  storeName,
77
90
  operation: async () => {
78
- const database = await databasePromise;
91
+ const database = await getDatabase();
79
92
  return (await database.get(storeName, key)) ?? null;
80
93
  },
81
94
  timeoutMs: operationTimeoutMs,
@@ -83,11 +96,12 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
83
96
  },
84
97
  async list() {
85
98
  return runBrowserStorageOperation({
99
+ failureMode: readFailureMode,
86
100
  fallback: [],
87
101
  operationName: "list object store",
88
102
  storeName,
89
103
  operation: async () => {
90
- const database = await databasePromise;
104
+ const database = await getDatabase();
91
105
  return database.getAll(storeName);
92
106
  },
93
107
  timeoutMs: operationTimeoutMs,
@@ -95,11 +109,12 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
95
109
  },
96
110
  async listEntries() {
97
111
  return runBrowserStorageOperation({
112
+ failureMode: readFailureMode,
98
113
  fallback: [],
99
114
  operationName: "list object-store entries",
100
115
  storeName,
101
116
  operation: async () => {
102
- const database = await databasePromise;
117
+ const database = await getDatabase();
103
118
  // Keys and values must come from one transaction. Separate database
104
119
  // reads could pair a key from one cross-tab storage state with a
105
120
  // value from another, causing retention code to delete the wrong key.
@@ -123,7 +138,7 @@ function createIdbKeyValueDriver({ databaseName, databaseVersion = 1, indexedDB
123
138
  },
124
139
  async setIf(key, value, predicate) {
125
140
  return runConditionalStorageOperation({
126
- databasePromise,
141
+ databasePromise: getDatabase(),
127
142
  operationName: "conditionally replace key",
128
143
  storeName,
129
144
  timeoutMs: operationTimeoutMs,
@@ -0,0 +1,10 @@
1
+ export * from "./readCache.js";
2
+ export * from "./pendingWrites.js";
3
+ export * from "./browserRecovery.js";
4
+ export * from "./browserCoordination.js";
5
+ export * from "./browserFallbackRecovery.js";
6
+ export * from "./durableCommandStore.js";
7
+ export { OfflineStorageError } from "./idbDriver.js";
8
+ export * from "./readBoundary.js";
9
+ export * from "./recoveryPolicy.js";
10
+ export * from "./pendingWriteSupersession.js";
package/dist-cjs/index.js CHANGED
@@ -23,3 +23,6 @@ __exportStar(require("./browserFallbackRecovery"), exports);
23
23
  __exportStar(require("./durableCommandStore"), exports);
24
24
  var idbDriver_1 = require("./idbDriver");
25
25
  Object.defineProperty(exports, "OfflineStorageError", { enumerable: true, get: function () { return idbDriver_1.OfflineStorageError; } });
26
+ __exportStar(require("./readBoundary"), exports);
27
+ __exportStar(require("./recoveryPolicy"), exports);
28
+ __exportStar(require("./pendingWriteSupersession"), exports);
@@ -0,0 +1,11 @@
1
+ import type { PendingWriteRecord, PendingWriteStore } from "./pendingWrites.js";
2
+ /**
3
+ * After remote acknowledgment, retire only rejected snapshots superseded by an
4
+ * explicit host-owned account/entity/field key. Never infer keys from payloads.
5
+ */
6
+ export declare function clearSupersededPendingWriteFailures<Payload>({ acknowledged, getSupersessionKey, isRejected, store, }: {
7
+ acknowledged: PendingWriteRecord<Payload>;
8
+ getSupersessionKey: (record: PendingWriteRecord<Payload>) => string | null;
9
+ isRejected: (record: PendingWriteRecord<Payload>) => boolean;
10
+ store: PendingWriteStore<Payload>;
11
+ }): Promise<number>;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.clearSupersededPendingWriteFailures = clearSupersededPendingWriteFailures;
4
+ /**
5
+ * After remote acknowledgment, retire only rejected snapshots superseded by an
6
+ * explicit host-owned account/entity/field key. Never infer keys from payloads.
7
+ */
8
+ async function clearSupersededPendingWriteFailures({ acknowledged, getSupersessionKey, isRejected, store, }) {
9
+ const key = getSupersessionKey(acknowledged);
10
+ if (key === null)
11
+ return 0;
12
+ let cleared = 0;
13
+ for (const previous of await store.list()) {
14
+ const older = previous.savedAt.localeCompare(acknowledged.savedAt) ||
15
+ previous.id.localeCompare(acknowledged.id);
16
+ if (previous.id === acknowledged.id ||
17
+ older >= 0 ||
18
+ previous.syncStatus !== "failed" ||
19
+ previous.syncingAt ||
20
+ !isRejected(previous) ||
21
+ getSupersessionKey(previous) !== key)
22
+ continue;
23
+ // Snapshot fencing protects a correction or claim accepted during the scan.
24
+ if (await store.clearIfSnapshotId(previous.id, previous.snapshotId))
25
+ cleared++;
26
+ }
27
+ return cleared;
28
+ }