@bayonai/offline 0.4.0 → 0.5.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 +6 -0
- package/README.md +24 -0
- package/dist/browserFallbackRecovery.d.ts +32 -0
- package/dist/browserFallbackRecovery.js +95 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist-cjs/browserFallbackRecovery.js +99 -0
- package/dist-cjs/index.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 - 2026-09-12
|
|
4
|
+
|
|
5
|
+
- Added `createBrowserOfflineFallbackRecovery`, a framework-free controller
|
|
6
|
+
for one-shot retry controls, browser reconnects, host-native reconnect
|
|
7
|
+
events, and optional pull-to-retry on standalone offline fallbacks.
|
|
8
|
+
|
|
3
9
|
## 0.4.0 - 2026-09-01
|
|
4
10
|
|
|
5
11
|
- Fixed cross-context replay claiming: a remote sync now runs only after an
|
package/README.md
CHANGED
|
@@ -156,6 +156,30 @@ the current tab as before. A contested lease retries silently with bounded
|
|
|
156
156
|
backoff. Remote write handlers must remain idempotent because leases do not
|
|
157
157
|
cover other devices or every browser failure mode.
|
|
158
158
|
|
|
159
|
+
## Static Offline Fallback Recovery
|
|
160
|
+
|
|
161
|
+
Standalone offline documents can reuse retry mechanics while keeping their
|
|
162
|
+
own markup, branding, copy, and host-native connectivity event name:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { createBrowserOfflineFallbackRecovery } from "@bayonai/offline";
|
|
166
|
+
|
|
167
|
+
const recovery = createBrowserOfflineFallbackRecovery({
|
|
168
|
+
control: document.querySelector<HTMLButtonElement>("#reload")!,
|
|
169
|
+
// Dispatch only after the host platform has validated connectivity.
|
|
170
|
+
retryEvent: "my-app:network-restored",
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
recovery.start();
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The controller disables the control after the first retry and changes its text
|
|
177
|
+
to `Reconnecting…`; it then reloads once. It listens to browser `online`, the
|
|
178
|
+
configured host event, and (by default) a 72px downward touch gesture beginning
|
|
179
|
+
at the top of the page. Set `pullThreshold: null` when the host owns touch
|
|
180
|
+
recovery. Static fallback documents must still bundle or inline this package
|
|
181
|
+
code; a browser cannot resolve a bare npm package specifier from `public/`.
|
|
182
|
+
|
|
159
183
|
Use `createOrderedPendingWriteExecutor` when a new write must never overtake a
|
|
160
184
|
previously queued write in the same user, workspace, or other durable scope.
|
|
161
185
|
Its `recover` and `run` methods share one lane: an unresolved predecessor keeps
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
/** Attempts recovery once. Repeated calls are ignored while navigation starts. */
|
|
15
|
+
retry: () => void;
|
|
16
|
+
start: () => void;
|
|
17
|
+
stop: () => void;
|
|
18
|
+
};
|
|
19
|
+
export type BrowserOfflineFallbackRecoveryOptions = {
|
|
20
|
+
control: OfflineFallbackRetryControl;
|
|
21
|
+
environment?: BrowserOfflineFallbackRecoveryEnvironment;
|
|
22
|
+
/** A host-native validation callback may dispatch this event after connectivity returns. */
|
|
23
|
+
retryEvent?: string;
|
|
24
|
+
retryingText?: string;
|
|
25
|
+
/** Set to `null` to omit touch pull-to-retry. */
|
|
26
|
+
pullThreshold?: number | null;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Owns generic recovery mechanics for a static offline fallback. Hosts retain
|
|
30
|
+
* all markup, copy, styling, and the choice of a native reconnect event.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createBrowserOfflineFallbackRecovery({ control, environment, pullThreshold, retryEvent, retryingText, }: BrowserOfflineFallbackRecoveryOptions): BrowserOfflineFallbackRecovery;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export const defaultOfflineFallbackRetryEvent = "app:network-restored";
|
|
2
|
+
export const defaultOfflineFallbackPullThreshold = 72;
|
|
3
|
+
/**
|
|
4
|
+
* Owns generic recovery mechanics for a static offline fallback. Hosts retain
|
|
5
|
+
* all markup, copy, styling, and the choice of a native reconnect event.
|
|
6
|
+
*/
|
|
7
|
+
export function createBrowserOfflineFallbackRecovery({ control, environment = createDefaultBrowserOfflineFallbackRecoveryEnvironment(), pullThreshold = defaultOfflineFallbackPullThreshold, retryEvent = defaultOfflineFallbackRetryEvent, retryingText = "Reconnecting…", }) {
|
|
8
|
+
let active = false;
|
|
9
|
+
let retrying = false;
|
|
10
|
+
let pullStartY = null;
|
|
11
|
+
function retry() {
|
|
12
|
+
if (retrying)
|
|
13
|
+
return;
|
|
14
|
+
retrying = true;
|
|
15
|
+
control.disabled = true;
|
|
16
|
+
control.textContent = retryingText;
|
|
17
|
+
try {
|
|
18
|
+
environment.reload();
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
// A failed navigation must leave the recovery action visible and usable.
|
|
22
|
+
console.error("[Bayon offline] Offline fallback reload failed.", { error });
|
|
23
|
+
retrying = false;
|
|
24
|
+
control.disabled = false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function handleTouchStart(event) {
|
|
28
|
+
const touchY = getSingleTouchY(event, "touches");
|
|
29
|
+
pullStartY = environment.getScrollY() === 0 ? touchY : null;
|
|
30
|
+
}
|
|
31
|
+
function handleTouchEnd(event) {
|
|
32
|
+
const touchY = getSingleTouchY(event, "changedTouches");
|
|
33
|
+
if (pullStartY !== null &&
|
|
34
|
+
touchY !== null &&
|
|
35
|
+
touchY - pullStartY >= (pullThreshold ?? Number.POSITIVE_INFINITY)) {
|
|
36
|
+
retry();
|
|
37
|
+
}
|
|
38
|
+
pullStartY = null;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
retry,
|
|
42
|
+
start() {
|
|
43
|
+
if (active)
|
|
44
|
+
return;
|
|
45
|
+
active = true;
|
|
46
|
+
environment.addEventListener("online", retry);
|
|
47
|
+
environment.addEventListener(retryEvent, retry);
|
|
48
|
+
if (pullThreshold !== null) {
|
|
49
|
+
environment.addEventListener("touchstart", handleTouchStart);
|
|
50
|
+
environment.addEventListener("touchend", handleTouchEnd);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
stop() {
|
|
54
|
+
if (!active)
|
|
55
|
+
return;
|
|
56
|
+
active = false;
|
|
57
|
+
environment.removeEventListener("online", retry);
|
|
58
|
+
environment.removeEventListener(retryEvent, retry);
|
|
59
|
+
if (pullThreshold !== null) {
|
|
60
|
+
environment.removeEventListener("touchstart", handleTouchStart);
|
|
61
|
+
environment.removeEventListener("touchend", handleTouchEnd);
|
|
62
|
+
}
|
|
63
|
+
pullStartY = null;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function createDefaultBrowserOfflineFallbackRecoveryEnvironment() {
|
|
68
|
+
const target = typeof window === "undefined" ? null : window;
|
|
69
|
+
return {
|
|
70
|
+
addEventListener(type, listener) {
|
|
71
|
+
target?.addEventListener(type, listener);
|
|
72
|
+
},
|
|
73
|
+
getScrollY() {
|
|
74
|
+
return target?.scrollY ?? 0;
|
|
75
|
+
},
|
|
76
|
+
reload() {
|
|
77
|
+
target?.location.reload();
|
|
78
|
+
},
|
|
79
|
+
removeEventListener(type, listener) {
|
|
80
|
+
target?.removeEventListener(type, listener);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function getSingleTouchY(event, key) {
|
|
85
|
+
if (!event || typeof event !== "object")
|
|
86
|
+
return null;
|
|
87
|
+
const touches = Reflect.get(event, key);
|
|
88
|
+
if (!touches || typeof touches !== "object")
|
|
89
|
+
return null;
|
|
90
|
+
const touch = Reflect.get(touches, 0);
|
|
91
|
+
if (!touch || typeof touch !== "object")
|
|
92
|
+
return null;
|
|
93
|
+
const clientY = Reflect.get(touch, "clientY");
|
|
94
|
+
return typeof clientY === "number" ? clientY : null;
|
|
95
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defaultOfflineFallbackPullThreshold = exports.defaultOfflineFallbackRetryEvent = void 0;
|
|
4
|
+
exports.createBrowserOfflineFallbackRecovery = createBrowserOfflineFallbackRecovery;
|
|
5
|
+
exports.defaultOfflineFallbackRetryEvent = "app:network-restored";
|
|
6
|
+
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…", }) {
|
|
12
|
+
let active = false;
|
|
13
|
+
let retrying = false;
|
|
14
|
+
let pullStartY = null;
|
|
15
|
+
function retry() {
|
|
16
|
+
if (retrying)
|
|
17
|
+
return;
|
|
18
|
+
retrying = true;
|
|
19
|
+
control.disabled = true;
|
|
20
|
+
control.textContent = retryingText;
|
|
21
|
+
try {
|
|
22
|
+
environment.reload();
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
// A failed navigation must leave the recovery action visible and usable.
|
|
26
|
+
console.error("[Bayon offline] Offline fallback reload failed.", { error });
|
|
27
|
+
retrying = false;
|
|
28
|
+
control.disabled = false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function handleTouchStart(event) {
|
|
32
|
+
const touchY = getSingleTouchY(event, "touches");
|
|
33
|
+
pullStartY = environment.getScrollY() === 0 ? touchY : null;
|
|
34
|
+
}
|
|
35
|
+
function handleTouchEnd(event) {
|
|
36
|
+
const touchY = getSingleTouchY(event, "changedTouches");
|
|
37
|
+
if (pullStartY !== null &&
|
|
38
|
+
touchY !== null &&
|
|
39
|
+
touchY - pullStartY >= (pullThreshold ?? Number.POSITIVE_INFINITY)) {
|
|
40
|
+
retry();
|
|
41
|
+
}
|
|
42
|
+
pullStartY = null;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
retry,
|
|
46
|
+
start() {
|
|
47
|
+
if (active)
|
|
48
|
+
return;
|
|
49
|
+
active = true;
|
|
50
|
+
environment.addEventListener("online", retry);
|
|
51
|
+
environment.addEventListener(retryEvent, retry);
|
|
52
|
+
if (pullThreshold !== null) {
|
|
53
|
+
environment.addEventListener("touchstart", handleTouchStart);
|
|
54
|
+
environment.addEventListener("touchend", handleTouchEnd);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
stop() {
|
|
58
|
+
if (!active)
|
|
59
|
+
return;
|
|
60
|
+
active = false;
|
|
61
|
+
environment.removeEventListener("online", retry);
|
|
62
|
+
environment.removeEventListener(retryEvent, retry);
|
|
63
|
+
if (pullThreshold !== null) {
|
|
64
|
+
environment.removeEventListener("touchstart", handleTouchStart);
|
|
65
|
+
environment.removeEventListener("touchend", handleTouchEnd);
|
|
66
|
+
}
|
|
67
|
+
pullStartY = null;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function createDefaultBrowserOfflineFallbackRecoveryEnvironment() {
|
|
72
|
+
const target = typeof window === "undefined" ? null : window;
|
|
73
|
+
return {
|
|
74
|
+
addEventListener(type, listener) {
|
|
75
|
+
target?.addEventListener(type, listener);
|
|
76
|
+
},
|
|
77
|
+
getScrollY() {
|
|
78
|
+
return target?.scrollY ?? 0;
|
|
79
|
+
},
|
|
80
|
+
reload() {
|
|
81
|
+
target?.location.reload();
|
|
82
|
+
},
|
|
83
|
+
removeEventListener(type, listener) {
|
|
84
|
+
target?.removeEventListener(type, listener);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function getSingleTouchY(event, key) {
|
|
89
|
+
if (!event || typeof event !== "object")
|
|
90
|
+
return null;
|
|
91
|
+
const touches = Reflect.get(event, key);
|
|
92
|
+
if (!touches || typeof touches !== "object")
|
|
93
|
+
return null;
|
|
94
|
+
const touch = Reflect.get(touches, 0);
|
|
95
|
+
if (!touch || typeof touch !== "object")
|
|
96
|
+
return null;
|
|
97
|
+
const clientY = Reflect.get(touch, "clientY");
|
|
98
|
+
return typeof clientY === "number" ? clientY : null;
|
|
99
|
+
}
|
package/dist-cjs/index.js
CHANGED
|
@@ -18,3 +18,4 @@ __exportStar(require("./readCache"), exports);
|
|
|
18
18
|
__exportStar(require("./pendingWrites"), exports);
|
|
19
19
|
__exportStar(require("./browserRecovery"), exports);
|
|
20
20
|
__exportStar(require("./browserCoordination"), exports);
|
|
21
|
+
__exportStar(require("./browserFallbackRecovery"), exports);
|