@bayonai/offline 0.12.0 → 0.15.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 +28 -1
- package/README.md +98 -1
- package/RECOVERY.md +3 -0
- package/dist/browserReadLifecycle.d.ts +5 -0
- package/dist/browserReadLifecycle.js +24 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/pendingWriteConsolidation.js +2 -1
- package/dist/pendingWrites.d.ts +3 -3
- package/dist/pendingWrites.js +20 -15
- package/dist/readCache.d.ts +38 -8
- package/dist/readCache.js +396 -41
- package/dist/readResourceBatch.d.ts +13 -0
- package/dist/readResourceBatch.js +116 -0
- package/dist/readResourceObserver.d.ts +57 -0
- package/dist/readResourceObserver.js +108 -0
- package/dist-cjs/browserReadLifecycle.d.ts +5 -0
- package/dist-cjs/browserReadLifecycle.js +27 -0
- package/dist-cjs/index.d.ts +3 -0
- package/dist-cjs/index.js +3 -0
- package/dist-cjs/pendingWriteConsolidation.js +2 -1
- package/dist-cjs/pendingWrites.d.ts +3 -3
- package/dist-cjs/pendingWrites.js +20 -15
- package/dist-cjs/readCache.d.ts +38 -8
- package/dist-cjs/readCache.js +396 -41
- package/dist-cjs/readResourceBatch.d.ts +13 -0
- package/dist-cjs/readResourceBatch.js +119 -0
- package/dist-cjs/readResourceObserver.d.ts +57 -0
- package/dist-cjs/readResourceObserver.js +111 -0
- package/package.json +1 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.observeReadResourceBatch = observeReadResourceBatch;
|
|
4
|
+
/** A mounted presentation barrier; child tasks retain sole network/cache ownership. */
|
|
5
|
+
function observeReadResourceBatch({ count, observe, freshForMs, now, current, canRetain, retiredError, subscribe, onUpdate }) {
|
|
6
|
+
let disposed = false;
|
|
7
|
+
let retired = false;
|
|
8
|
+
let presented = false;
|
|
9
|
+
let strict = false;
|
|
10
|
+
let initialRetry;
|
|
11
|
+
let pending;
|
|
12
|
+
let failure;
|
|
13
|
+
let stop = () => { };
|
|
14
|
+
const children = [];
|
|
15
|
+
const disposedChildren = new Set();
|
|
16
|
+
const detach = () => { const cleanup = stop; stop = () => { }; cleanup(); };
|
|
17
|
+
const disposeChildren = () => children.forEach(child => {
|
|
18
|
+
if (!disposedChildren.has(child)) {
|
|
19
|
+
disposedChildren.add(child);
|
|
20
|
+
child.dispose();
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const values = new Array(count);
|
|
24
|
+
const received = new Set();
|
|
25
|
+
const ages = new Array(count).fill(-Infinity);
|
|
26
|
+
const active = () => !disposed && !retired && current();
|
|
27
|
+
const retire = () => { retired = true; detach(); disposeChildren(); };
|
|
28
|
+
for (let index = 0; index < count; index++) {
|
|
29
|
+
const child = observe(index, update => {
|
|
30
|
+
if (disposed || retired)
|
|
31
|
+
return;
|
|
32
|
+
if (update.status === "invalidated" || update.status === "expired") {
|
|
33
|
+
onUpdate({ status: "invalidated" });
|
|
34
|
+
retire();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (!current())
|
|
38
|
+
return;
|
|
39
|
+
if ("data" in update) {
|
|
40
|
+
received.add(index);
|
|
41
|
+
values[index] = update.data;
|
|
42
|
+
if (!canRetain(update.data))
|
|
43
|
+
strict = true;
|
|
44
|
+
}
|
|
45
|
+
// Denial retires the group even before every initial source has settled.
|
|
46
|
+
if (update.status === "failed") {
|
|
47
|
+
onUpdate(update);
|
|
48
|
+
presented = false;
|
|
49
|
+
retire();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (update.status === "retry") {
|
|
53
|
+
if (presented)
|
|
54
|
+
failure = update;
|
|
55
|
+
else
|
|
56
|
+
initialRetry = { error: update.error };
|
|
57
|
+
}
|
|
58
|
+
}, createdAt => { ages[index] = createdAt; });
|
|
59
|
+
children.push(child);
|
|
60
|
+
if (retired) {
|
|
61
|
+
disposeChildren();
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const refresh = (force = false) => {
|
|
66
|
+
if (!active() || !presented || strict)
|
|
67
|
+
return Promise.resolve();
|
|
68
|
+
if (pending)
|
|
69
|
+
return pending;
|
|
70
|
+
if (!force && ages.every((age, index) => now() - age < freshForMs[index]))
|
|
71
|
+
return Promise.resolve();
|
|
72
|
+
// Claim the barrier before a presentation callback can request it again.
|
|
73
|
+
pending = Promise.resolve().then(async () => {
|
|
74
|
+
if (!active() || strict)
|
|
75
|
+
return;
|
|
76
|
+
failure = undefined;
|
|
77
|
+
onUpdate({ status: "refreshing" });
|
|
78
|
+
if (!active() || strict)
|
|
79
|
+
return;
|
|
80
|
+
await Promise.all(children.map(child => child.refresh?.({ force })));
|
|
81
|
+
if (!active())
|
|
82
|
+
return;
|
|
83
|
+
if (failure)
|
|
84
|
+
onUpdate(failure);
|
|
85
|
+
else {
|
|
86
|
+
// Never expose a successful sibling beside an older failed member.
|
|
87
|
+
onUpdate({ status: "fresh", data: [...values] });
|
|
88
|
+
if (strict)
|
|
89
|
+
detach();
|
|
90
|
+
}
|
|
91
|
+
}).finally(() => { pending = undefined; });
|
|
92
|
+
return pending;
|
|
93
|
+
};
|
|
94
|
+
if (!retired)
|
|
95
|
+
stop = subscribe(() => refresh());
|
|
96
|
+
const promise = Promise.allSettled(children.map(child => child.promise)).then(results => {
|
|
97
|
+
if (!active())
|
|
98
|
+
throw retiredError();
|
|
99
|
+
const rejected = results.find(result => result.status === "rejected");
|
|
100
|
+
if (rejected?.status === "rejected")
|
|
101
|
+
throw rejected.reason;
|
|
102
|
+
const data = results.map(result => result.value);
|
|
103
|
+
data.forEach((value, index) => { if (!received.has(index))
|
|
104
|
+
values[index] = value; });
|
|
105
|
+
presented = true;
|
|
106
|
+
// Presentation follows accepted canonical updates, while the promise keeps
|
|
107
|
+
// each shared source caller's original result (including duplicate positions).
|
|
108
|
+
onUpdate({ status: initialRetry === undefined ? "fresh" : "retained", data: [...values] });
|
|
109
|
+
if (initialRetry !== undefined && active())
|
|
110
|
+
onUpdate({ status: "retry", error: initialRetry.error });
|
|
111
|
+
if (strict)
|
|
112
|
+
detach();
|
|
113
|
+
return data;
|
|
114
|
+
});
|
|
115
|
+
return { promise,
|
|
116
|
+
refresh: ({ force = true } = {}) => active() && presented && !strict ? refresh(force) : undefined,
|
|
117
|
+
dispose() { disposed = true; detach(); disposeChildren(); },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type ReadResourceUpdate<T> = {
|
|
2
|
+
status: "retained" | "fresh";
|
|
3
|
+
data: T;
|
|
4
|
+
} | {
|
|
5
|
+
status: "refreshing";
|
|
6
|
+
} | {
|
|
7
|
+
status: "retry";
|
|
8
|
+
error: unknown;
|
|
9
|
+
} | {
|
|
10
|
+
status: "failed";
|
|
11
|
+
error: unknown;
|
|
12
|
+
} | {
|
|
13
|
+
status: "expired";
|
|
14
|
+
} | {
|
|
15
|
+
status: "invalidated";
|
|
16
|
+
};
|
|
17
|
+
export type ReadResourceSnapshot<T> = {
|
|
18
|
+
data: T;
|
|
19
|
+
confirmedAt: number;
|
|
20
|
+
};
|
|
21
|
+
export type ReadResourceObservation<T> = {
|
|
22
|
+
promise: Promise<T>;
|
|
23
|
+
refresh?: (options?: {
|
|
24
|
+
force?: boolean;
|
|
25
|
+
}) => Promise<void> | undefined;
|
|
26
|
+
dispose: () => void;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Mounted presentation only: hosts retain cache, transport, identity and initial
|
|
30
|
+
* read policy. Accepting a snapshot preserves its source age; a successful
|
|
31
|
+
* refresh does not require persistence to have survived an unrelated write.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createReadResourceObserver<T, Snapshot>({ enabled, current, now, freshForMs, canRetain, isConnected, canonical, captureSnapshot, read, retireSnapshot, classifyFailure, subscribeWake, onUpdate, onData, onError, }: {
|
|
34
|
+
enabled?: boolean;
|
|
35
|
+
current: () => boolean;
|
|
36
|
+
now: () => number;
|
|
37
|
+
freshForMs: number;
|
|
38
|
+
canRetain: (data: T) => boolean;
|
|
39
|
+
isConnected?: () => boolean;
|
|
40
|
+
canonical: () => ReadResourceSnapshot<T> | undefined;
|
|
41
|
+
captureSnapshot: () => Snapshot;
|
|
42
|
+
read: () => Promise<ReadResourceSnapshot<T>>;
|
|
43
|
+
retireSnapshot: (snapshot: Snapshot) => Promise<void>;
|
|
44
|
+
classifyFailure: (error: unknown) => "transient" | "terminal" | "superseded" | "retired";
|
|
45
|
+
subscribeWake?: (listener: () => Promise<void>) => () => void;
|
|
46
|
+
onUpdate: (update: ReadResourceUpdate<T>) => void;
|
|
47
|
+
onData?: (data: T) => void;
|
|
48
|
+
onError?: (error: unknown) => void;
|
|
49
|
+
}): {
|
|
50
|
+
accept: (update: ReadResourceUpdate<T>, age?: number) => void;
|
|
51
|
+
canRefresh: () => boolean;
|
|
52
|
+
refresh: ({ force }?: {
|
|
53
|
+
force?: boolean | undefined;
|
|
54
|
+
}) => Promise<void> | undefined;
|
|
55
|
+
retire(): void;
|
|
56
|
+
dispose(): void;
|
|
57
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createReadResourceObserver = createReadResourceObserver;
|
|
4
|
+
/**
|
|
5
|
+
* Mounted presentation only: hosts retain cache, transport, identity and initial
|
|
6
|
+
* read policy. Accepting a snapshot preserves its source age; a successful
|
|
7
|
+
* refresh does not require persistence to have survived an unrelated write.
|
|
8
|
+
*/
|
|
9
|
+
function createReadResourceObserver({ enabled = true, current, now, freshForMs, canRetain, isConnected = () => true, canonical, captureSnapshot, read, retireSnapshot, classifyFailure, subscribeWake, onUpdate, onData, onError, }) {
|
|
10
|
+
let disposed = false;
|
|
11
|
+
let retired = false;
|
|
12
|
+
let confirmedAt;
|
|
13
|
+
let retainable = true;
|
|
14
|
+
let pending;
|
|
15
|
+
let stop = () => { };
|
|
16
|
+
const detach = () => { const cleanup = stop; stop = () => { }; cleanup(); };
|
|
17
|
+
const active = () => !disposed && !retired && current();
|
|
18
|
+
const canRefresh = () => enabled && active() && retainable && confirmedAt !== undefined;
|
|
19
|
+
const accept = (update, age = now()) => {
|
|
20
|
+
if (!active())
|
|
21
|
+
return;
|
|
22
|
+
if ("data" in update) {
|
|
23
|
+
confirmedAt = age;
|
|
24
|
+
retainable = canRetain(update.data);
|
|
25
|
+
onData?.(update.data);
|
|
26
|
+
}
|
|
27
|
+
if (active())
|
|
28
|
+
onUpdate(update);
|
|
29
|
+
};
|
|
30
|
+
const adoptCanonical = () => {
|
|
31
|
+
const snapshot = canonical();
|
|
32
|
+
if (!snapshot || !active() || now() - snapshot.confirmedAt >= freshForMs)
|
|
33
|
+
return false;
|
|
34
|
+
accept({ status: "fresh", data: snapshot.data }, snapshot.confirmedAt);
|
|
35
|
+
return true;
|
|
36
|
+
};
|
|
37
|
+
const run = async (force) => {
|
|
38
|
+
if (!canRefresh())
|
|
39
|
+
return;
|
|
40
|
+
if (!force && now() - confirmedAt < freshForMs)
|
|
41
|
+
return;
|
|
42
|
+
if (!isConnected()) {
|
|
43
|
+
accept({ status: "retry", error: new Error("Resource unavailable while disconnected") });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (!force && adoptCanonical())
|
|
47
|
+
return;
|
|
48
|
+
accept({ status: "refreshing" });
|
|
49
|
+
if (!canRefresh())
|
|
50
|
+
return;
|
|
51
|
+
const snapshot = captureSnapshot();
|
|
52
|
+
let result;
|
|
53
|
+
try {
|
|
54
|
+
result = await read();
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (!active())
|
|
58
|
+
return;
|
|
59
|
+
const failure = classifyFailure(error);
|
|
60
|
+
if (failure === "superseded") {
|
|
61
|
+
adoptCanonical();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (failure === "retired")
|
|
65
|
+
return;
|
|
66
|
+
if (onError)
|
|
67
|
+
onError(error);
|
|
68
|
+
else
|
|
69
|
+
console.error("[Bayon offline] Mounted resource refresh failed.", { error });
|
|
70
|
+
if (failure === "transient")
|
|
71
|
+
accept({ status: "retry", error });
|
|
72
|
+
else {
|
|
73
|
+
try {
|
|
74
|
+
await retireSnapshot(snapshot);
|
|
75
|
+
}
|
|
76
|
+
catch (storageError) {
|
|
77
|
+
// Storage cleanup cannot turn a known denial into retained presentation.
|
|
78
|
+
console.error("[Bayon offline] Unable to retire a mounted read snapshot.", { error: storageError });
|
|
79
|
+
}
|
|
80
|
+
if (!active())
|
|
81
|
+
return;
|
|
82
|
+
confirmedAt = undefined;
|
|
83
|
+
accept({ status: "failed", error });
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
accept({ status: "fresh", data: result.data }, result.confirmedAt);
|
|
88
|
+
};
|
|
89
|
+
const refresh = ({ force = true } = {}) => {
|
|
90
|
+
if (!canRefresh())
|
|
91
|
+
return;
|
|
92
|
+
// Install ownership before callbacks can synchronously request another read.
|
|
93
|
+
if (!pending)
|
|
94
|
+
pending = Promise.resolve().then(() => run(force)).finally(() => { pending = undefined; });
|
|
95
|
+
return pending;
|
|
96
|
+
};
|
|
97
|
+
if (enabled && subscribeWake)
|
|
98
|
+
stop = subscribeWake(() => refresh({ force: false }) ?? Promise.resolve());
|
|
99
|
+
return {
|
|
100
|
+
accept, canRefresh, refresh,
|
|
101
|
+
retire() {
|
|
102
|
+
if (disposed || retired)
|
|
103
|
+
return;
|
|
104
|
+
retired = true;
|
|
105
|
+
detach();
|
|
106
|
+
// Clearing protected presentation is safe even after the host changed scope.
|
|
107
|
+
onUpdate({ status: "invalidated" });
|
|
108
|
+
},
|
|
109
|
+
dispose() { disposed = true; detach(); },
|
|
110
|
+
};
|
|
111
|
+
}
|