@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.
- package/CHANGELOG.md +39 -1
- package/README.md +107 -16
- package/RECOVERY.md +64 -0
- package/dist/browserFallbackRecovery.d.ts +10 -9
- package/dist/browserFallbackRecovery.js +100 -28
- package/dist/browserRecovery.d.ts +2 -2
- package/dist/durableCommandStore.d.ts +1 -1
- package/dist/idbDriver.d.ts +3 -1
- package/dist/idbDriver.js +29 -14
- package/dist/index.d.ts +10 -7
- package/dist/index.js +3 -0
- package/dist/pendingWriteSupersession.d.ts +11 -0
- package/dist/pendingWriteSupersession.js +25 -0
- package/dist/pendingWrites.d.ts +6 -2
- package/dist/pendingWrites.js +45 -11
- package/dist/readBoundary.d.ts +13 -0
- package/dist/readBoundary.js +23 -0
- package/dist/readCache.d.ts +26 -5
- package/dist/readCache.js +113 -19
- package/dist/recoveryPolicy.d.ts +20 -0
- package/dist/recoveryPolicy.js +73 -0
- package/dist-cjs/browserCoordination.d.ts +23 -0
- package/dist-cjs/browserFallbackRecovery.d.ts +33 -0
- package/dist-cjs/browserFallbackRecovery.js +100 -28
- package/dist-cjs/browserRecovery.d.ts +42 -0
- package/dist-cjs/durableCommandStore.d.ts +49 -0
- package/dist-cjs/idbDriver.d.ts +28 -0
- package/dist-cjs/idbDriver.js +29 -14
- package/dist-cjs/index.d.ts +10 -0
- package/dist-cjs/index.js +3 -0
- package/dist-cjs/pendingWriteSupersession.d.ts +11 -0
- package/dist-cjs/pendingWriteSupersession.js +28 -0
- package/dist-cjs/pendingWrites.d.ts +179 -0
- package/dist-cjs/pendingWrites.js +45 -11
- package/dist-cjs/readBoundary.d.ts +13 -0
- package/dist-cjs/readBoundary.js +26 -0
- package/dist-cjs/readCache.d.ts +112 -0
- package/dist-cjs/readCache.js +113 -19
- package/dist-cjs/recoveryPolicy.d.ts +20 -0
- package/dist-cjs/recoveryPolicy.js +78 -0
- 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
|
-
|
|
9
|
-
|
|
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
|
|
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 ?
|
|
16
|
-
function
|
|
17
|
-
if (
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
44
|
-
|
|
103
|
+
touchY - pullStartY >= touchThreshold)
|
|
104
|
+
refresh();
|
|
105
|
+
pullStartY = null;
|
|
106
|
+
}
|
|
107
|
+
function handleTouchCancel() {
|
|
45
108
|
pullStartY = null;
|
|
46
109
|
}
|
|
47
110
|
return {
|
|
48
|
-
|
|
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",
|
|
54
|
-
environment.addEventListener(retryEvent,
|
|
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",
|
|
65
|
-
environment.removeEventListener(retryEvent,
|
|
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;
|
package/dist-cjs/idbDriver.js
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|