@object-ui/mobile 3.3.2 → 3.4.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 +34 -0
- package/dist/createOfflineDataSource.d.ts +62 -0
- package/dist/createOfflineDataSource.d.ts.map +1 -0
- package/dist/createOfflineDataSource.js +146 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/offlineQueue.d.ts +78 -0
- package/dist/offlineQueue.d.ts.map +1 -0
- package/dist/offlineQueue.js +110 -0
- package/dist/serviceWorkerSource.d.ts +39 -0
- package/dist/serviceWorkerSource.d.ts.map +1 -0
- package/dist/serviceWorkerSource.js +103 -0
- package/dist/useOfflineSync.d.ts +29 -0
- package/dist/useOfflineSync.d.ts.map +1 -0
- package/dist/useOfflineSync.js +61 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# @object-ui/mobile
|
|
2
2
|
|
|
3
|
+
## 3.4.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a2d7023: End-user feature batch — forms, designer history, import/export, and PWA offline sync.
|
|
8
|
+
|
|
9
|
+
**Forms (`@object-ui/fields`, `@object-ui/providers`)**
|
|
10
|
+
- `FileField`: native `<input capture="environment">` camera capture for mobile devices, plus a uploading-progress indicator driven by `UploadProvider`.
|
|
11
|
+
- `ImageField`: per-image inline crop/rotate via the lazy-loaded `ImageCropperDialog` (canvas-based, zero new deps).
|
|
12
|
+
- New `UploadProvider` in `@object-ui/providers` with pluggable adapters for S3 and Azure Blob (plus the default object-URL adapter for local previews). XHR-based with progress, abort, and retry.
|
|
13
|
+
- `LookupField`: `lookup.dependsOn: string | string[]` to chain dependent lookups (e.g. State depends on Country); the trigger is gated until parent values are present and the OData `$filter` is built automatically.
|
|
14
|
+
|
|
15
|
+
**Container-aware widget widths (`@object-ui/components`)**
|
|
16
|
+
- New `useResizeObserver(ref)` hook exposing `{ width, height }` of any element. SSR-safe; reads the initial size via `getBoundingClientRect`.
|
|
17
|
+
- `plugin-gantt` and `plugin-kanban` now react to their container size instead of `window.innerWidth`, so they behave correctly inside split panels and dashboards.
|
|
18
|
+
|
|
19
|
+
**Designer history (`@object-ui/plugin-designer`)**
|
|
20
|
+
- `useUndoRedo` (and therefore `useDesignerHistory`) gains `persistKey` + `storage` options to round-trip the undo/redo stack through `sessionStorage`, plus a `clearPersisted()` cleanup helper. Drafts now survive accidental tab refreshes.
|
|
21
|
+
- New `<HistoryPanel>` component renders the timeline visually with one-click jump-to-checkpoint via the new `jumpTo(index)` API.
|
|
22
|
+
|
|
23
|
+
**Import wizard (`@object-ui/plugin-grid`)**
|
|
24
|
+
- Saved column-mapping templates: name, save, re-apply, and delete via a new template bar in the mapping step. Persisted under `objectui:import-templates:${objectName}` (override via `templateStorageKey` / `templateStorage`).
|
|
25
|
+
- Inline validation correction: cells with errors in the preview step are now editable; corrections feed straight into the import without requiring a re-upload, with green-bar status indicators for fixed rows.
|
|
26
|
+
|
|
27
|
+
**PWA offline sync (`@object-ui/mobile`)**
|
|
28
|
+
- New `MemoryOfflineQueue` / `IndexedDbOfflineQueue` (`createOfflineQueue()` picks the best backend) backed by IndexedDB.
|
|
29
|
+
- `createOfflineDataSource(inner, { queue })` wraps any DataSource so mutations issued while offline (or that fail with a network-style error) are queued and replayed in order on reconnect. Includes `replay()`, `drop()`, `clear()`, `pending()`, an `onChange` notifier, and an opt-in `resolveConflict` hook for stale-write conflicts.
|
|
30
|
+
- New `useOfflineSync(source)` hook exposes `{ isOnline, pending, isReplaying, replay, drop, clear }` and auto-replays on the browser's `online` event.
|
|
31
|
+
- `getServiceWorkerSource(opts)` emits a customisable Service Worker that pre-caches the app shell, applies network-first to API requests, and broadcasts `REPLAY_QUEUE` to clients on Background Sync. `requestBackgroundSync(tag)` registers a one-shot sync from the page.
|
|
32
|
+
|
|
33
|
+
- Updated dependencies [f1ca238]
|
|
34
|
+
- Updated dependencies [de881ef]
|
|
35
|
+
- @object-ui/types@3.4.0
|
|
36
|
+
|
|
3
37
|
## 3.3.2
|
|
4
38
|
|
|
5
39
|
### Patch Changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* `createOfflineDataSource` — wraps any DataSource so that mutations made
|
|
6
|
+
* while the network is offline (or while the inner source rejects with a
|
|
7
|
+
* network-style error) are persisted to an offline queue and replayed
|
|
8
|
+
* automatically when connectivity returns.
|
|
9
|
+
*
|
|
10
|
+
* Reads (`find` / `findOne`) are pass-through; the optional `cache` hook
|
|
11
|
+
* lets callers opt into local caching but is not built in here.
|
|
12
|
+
*/
|
|
13
|
+
import { type OfflineQueueBackend, type OfflineOperation } from './offlineQueue';
|
|
14
|
+
/** Minimal DataSource shape we care about. */
|
|
15
|
+
export interface QueueableDataSource {
|
|
16
|
+
find?: (object: string, query?: any) => Promise<any>;
|
|
17
|
+
findOne?: (object: string, id: string | number, query?: any) => Promise<any>;
|
|
18
|
+
create?: (object: string, payload: any) => Promise<any>;
|
|
19
|
+
update?: (object: string, id: string | number, payload: any) => Promise<any>;
|
|
20
|
+
delete?: (object: string, id: string | number) => Promise<any>;
|
|
21
|
+
[k: string]: any;
|
|
22
|
+
}
|
|
23
|
+
export interface OfflineDataSourceOptions {
|
|
24
|
+
queue: OfflineQueueBackend;
|
|
25
|
+
/** Custom predicate to detect network failure. Defaults to instanceof TypeError + 5xx. */
|
|
26
|
+
isNetworkError?: (err: unknown) => boolean;
|
|
27
|
+
/** Override online detection. Defaults to `navigator.onLine`. */
|
|
28
|
+
isOnline?: () => boolean;
|
|
29
|
+
/** Called whenever queue contents change (enqueue / replay / drop). */
|
|
30
|
+
onChange?: (ops: OfflineOperation[]) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Optional conflict resolver. Returns `'retry'` to leave the op in place,
|
|
33
|
+
* `'drop'` to discard, or a fresh payload to retry with overrides.
|
|
34
|
+
*/
|
|
35
|
+
resolveConflict?: (op: OfflineOperation, error: unknown) => Promise<'retry' | 'drop' | {
|
|
36
|
+
payload: any;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
/** A DataSource wrapper that queues mutations while offline. */
|
|
40
|
+
export interface OfflineDataSource extends QueueableDataSource {
|
|
41
|
+
/** Number of pending operations (sync, may lag a tick — call `pendingCount()` for fresh). */
|
|
42
|
+
pendingCount(): Promise<number>;
|
|
43
|
+
/** Replay all pending ops in order. Returns counts. */
|
|
44
|
+
replay(): Promise<{
|
|
45
|
+
succeeded: number;
|
|
46
|
+
failed: number;
|
|
47
|
+
remaining: number;
|
|
48
|
+
}>;
|
|
49
|
+
/** Drop a specific queued op (e.g. from a UI). */
|
|
50
|
+
drop(opId: string): Promise<void>;
|
|
51
|
+
/** Drop all queued ops (e.g. "discard local changes"). */
|
|
52
|
+
clear(): Promise<void>;
|
|
53
|
+
/** Inspect all queued ops. */
|
|
54
|
+
pending(): Promise<OfflineOperation[]>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Wrap an inner DataSource. Mutations that fail with a network-style error
|
|
58
|
+
* — or are issued while offline — are stored in the queue and replayed on
|
|
59
|
+
* reconnect.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createOfflineDataSource(inner: QueueableDataSource, options: OfflineDataSourceOptions): OfflineDataSource;
|
|
62
|
+
//# sourceMappingURL=createOfflineDataSource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createOfflineDataSource.d.ts","sourceRoot":"","sources":["../src/createOfflineDataSource.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,KAAK,mBAAmB,EAAE,KAAK,gBAAgB,EAAgB,MAAM,gBAAgB,CAAC;AAE/F,8CAA8C;AAC9C,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IACrD,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7E,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IACxD,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7E,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IAC/D,CAAC,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;CAClB;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,mBAAmB,CAAC;IAC3B,0FAA0F;IAC1F,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;IAC3C,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC;IACzB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAC;IAC7C;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,EAAE,EAAE,gBAAgB,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,GAAG,MAAM,GAAG;QAAE,OAAO,EAAE,GAAG,CAAA;KAAE,CAAC,CAAC;CAC1G;AAiBD,gEAAgE;AAChE,MAAM,WAAW,iBAAkB,SAAQ,mBAAmB;IAC5D,6FAA6F;IAC7F,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAChC,uDAAuD;IACvD,MAAM,IAAI,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5E,kDAAkD;IAClD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,0DAA0D;IAC1D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,8BAA8B;IAC9B,OAAO,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC;CACxC;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,wBAAwB,GAChC,iBAAiB,CA6GnB"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* `createOfflineDataSource` — wraps any DataSource so that mutations made
|
|
6
|
+
* while the network is offline (or while the inner source rejects with a
|
|
7
|
+
* network-style error) are persisted to an offline queue and replayed
|
|
8
|
+
* automatically when connectivity returns.
|
|
9
|
+
*
|
|
10
|
+
* Reads (`find` / `findOne`) are pass-through; the optional `cache` hook
|
|
11
|
+
* lets callers opt into local caching but is not built in here.
|
|
12
|
+
*/
|
|
13
|
+
import { generateOpId } from './offlineQueue';
|
|
14
|
+
const defaultIsNetworkError = (err) => {
|
|
15
|
+
if (!err)
|
|
16
|
+
return false;
|
|
17
|
+
if (err instanceof TypeError)
|
|
18
|
+
return true;
|
|
19
|
+
const e = err;
|
|
20
|
+
if (e.code === 'ENOTFOUND' || e.code === 'ECONNREFUSED' || e.code === 'NETWORK_ERROR')
|
|
21
|
+
return true;
|
|
22
|
+
if (typeof e.status === 'number' && e.status >= 500)
|
|
23
|
+
return true;
|
|
24
|
+
if (typeof e.message === 'string' && /network|fetch|offline/i.test(e.message))
|
|
25
|
+
return true;
|
|
26
|
+
return false;
|
|
27
|
+
};
|
|
28
|
+
const defaultIsOnline = () => {
|
|
29
|
+
if (typeof navigator === 'undefined')
|
|
30
|
+
return true;
|
|
31
|
+
return navigator.onLine !== false;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Wrap an inner DataSource. Mutations that fail with a network-style error
|
|
35
|
+
* — or are issued while offline — are stored in the queue and replayed on
|
|
36
|
+
* reconnect.
|
|
37
|
+
*/
|
|
38
|
+
export function createOfflineDataSource(inner, options) {
|
|
39
|
+
const { queue, onChange } = options;
|
|
40
|
+
const isNetErr = options.isNetworkError ?? defaultIsNetworkError;
|
|
41
|
+
const isOnline = options.isOnline ?? defaultIsOnline;
|
|
42
|
+
const notify = async () => {
|
|
43
|
+
if (!onChange)
|
|
44
|
+
return;
|
|
45
|
+
try {
|
|
46
|
+
onChange(await queue.list());
|
|
47
|
+
}
|
|
48
|
+
catch { /* ignore */ }
|
|
49
|
+
};
|
|
50
|
+
const enqueue = async (op) => {
|
|
51
|
+
const full = {
|
|
52
|
+
id: generateOpId(),
|
|
53
|
+
enqueuedAt: Date.now(),
|
|
54
|
+
attempts: 0,
|
|
55
|
+
...op,
|
|
56
|
+
};
|
|
57
|
+
await queue.enqueue(full);
|
|
58
|
+
await notify();
|
|
59
|
+
return full;
|
|
60
|
+
};
|
|
61
|
+
// Optimistic helper: try the inner mutation, queue on network failure.
|
|
62
|
+
const guarded = async (op, runner) => {
|
|
63
|
+
if (!isOnline()) {
|
|
64
|
+
const queued = await enqueue(op);
|
|
65
|
+
return { queued: true, op: queued };
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return await runner();
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (isNetErr(err)) {
|
|
72
|
+
const queued = await enqueue(op);
|
|
73
|
+
return { queued: true, op: queued };
|
|
74
|
+
}
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const wrapped = {
|
|
79
|
+
...inner,
|
|
80
|
+
find: inner.find?.bind(inner),
|
|
81
|
+
findOne: inner.findOne?.bind(inner),
|
|
82
|
+
create: inner.create
|
|
83
|
+
? (object, payload) => guarded({ op: 'create', object, payload }, () => inner.create(object, payload))
|
|
84
|
+
: undefined,
|
|
85
|
+
update: inner.update
|
|
86
|
+
? (object, id, payload) => guarded({ op: 'update', object, recordId: id, payload }, () => inner.update(object, id, payload))
|
|
87
|
+
: undefined,
|
|
88
|
+
delete: inner.delete
|
|
89
|
+
? (object, id) => guarded({ op: 'delete', object, recordId: id }, () => inner.delete(object, id))
|
|
90
|
+
: undefined,
|
|
91
|
+
async pendingCount() { return (await queue.list()).length; },
|
|
92
|
+
async pending() { return queue.list(); },
|
|
93
|
+
async drop(opId) { await queue.remove(opId); await notify(); },
|
|
94
|
+
async clear() { await queue.clear(); await notify(); },
|
|
95
|
+
async replay() {
|
|
96
|
+
let succeeded = 0;
|
|
97
|
+
let failed = 0;
|
|
98
|
+
const ops = await queue.list();
|
|
99
|
+
for (const op of ops) {
|
|
100
|
+
try {
|
|
101
|
+
if (op.op === 'create' && inner.create)
|
|
102
|
+
await inner.create(op.object, op.payload);
|
|
103
|
+
else if (op.op === 'update' && inner.update)
|
|
104
|
+
await inner.update(op.object, op.recordId, op.payload);
|
|
105
|
+
else if (op.op === 'delete' && inner.delete)
|
|
106
|
+
await inner.delete(op.object, op.recordId);
|
|
107
|
+
else
|
|
108
|
+
throw new Error(`Unsupported op: ${op.op}`);
|
|
109
|
+
await queue.remove(op.id);
|
|
110
|
+
succeeded++;
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
// Non-network failures may be conflicts: ask the resolver.
|
|
114
|
+
if (!isNetErr(err) && options.resolveConflict) {
|
|
115
|
+
const decision = await options.resolveConflict(op, err);
|
|
116
|
+
if (decision === 'drop') {
|
|
117
|
+
await queue.remove(op.id);
|
|
118
|
+
succeeded++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (typeof decision === 'object' && 'payload' in decision) {
|
|
122
|
+
const next = { ...op, payload: decision.payload, attempts: op.attempts + 1 };
|
|
123
|
+
await queue.update(next);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// 'retry' falls through to bumping attempts below.
|
|
127
|
+
}
|
|
128
|
+
const next = {
|
|
129
|
+
...op,
|
|
130
|
+
attempts: op.attempts + 1,
|
|
131
|
+
lastError: err instanceof Error ? err.message : String(err),
|
|
132
|
+
};
|
|
133
|
+
await queue.update(next);
|
|
134
|
+
failed++;
|
|
135
|
+
// For network errors stop processing — we'll likely fail the rest too.
|
|
136
|
+
if (isNetErr(err))
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const remaining = (await queue.list()).length;
|
|
141
|
+
await notify();
|
|
142
|
+
return { succeeded, failed, remaining };
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
return wrapped;
|
|
146
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,10 @@ export { MobileProvider, type MobileProviderProps } from './MobileProvider';
|
|
|
28
28
|
export { ResponsiveContainer, type ResponsiveContainerProps } from './ResponsiveContainer';
|
|
29
29
|
export { generatePWAManifest } from './pwa';
|
|
30
30
|
export { registerServiceWorker, type ServiceWorkerConfig } from './serviceWorker';
|
|
31
|
+
export { createOfflineQueue, IndexedDbOfflineQueue, MemoryOfflineQueue, generateOpId, type OfflineOperation, type OfflineQueueBackend, } from './offlineQueue';
|
|
32
|
+
export { createOfflineDataSource, type OfflineDataSource, type OfflineDataSourceOptions, type QueueableDataSource, } from './createOfflineDataSource';
|
|
33
|
+
export { useOfflineSync, type OfflineSyncState } from './useOfflineSync';
|
|
34
|
+
export { getServiceWorkerSource, requestBackgroundSync, type ServiceWorkerSourceOptions, } from './serviceWorkerSource';
|
|
31
35
|
export { BREAKPOINTS, resolveResponsiveValue } from './breakpoints';
|
|
32
36
|
export type { BreakpointName, ResponsiveValue, ResponsiveConfig, MobileOverrides, PWAConfig, PWAIcon, CacheStrategy, OfflineConfig, OfflineRoute, GestureType, GestureConfig, GestureContext, MobileComponentConfig, SpecGestureConfig, SwipeGestureConfig, PinchGestureConfig, LongPressGestureConfig, TouchInteraction, TouchTargetConfig, } from '@object-ui/types';
|
|
33
37
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,KAAK,oBAAoB,EAAE,KAAK,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AACrH,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACtG,OAAO,EAAE,gBAAgB,EAAE,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,KAAK,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAC3F,OAAO,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAE,qBAAqB,EAAE,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAGpE,YAAY,EACV,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,OAAO,EACP,aAAa,EACb,aAAa,EACb,YAAY,EACZ,WAAW,EACX,aAAa,EACb,cAAc,EACd,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,KAAK,oBAAoB,EAAE,KAAK,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AACrH,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACtG,OAAO,EAAE,gBAAgB,EAAE,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,KAAK,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAC3F,OAAO,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAE,qBAAqB,EAAE,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAClF,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EAClB,YAAY,EACZ,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,uBAAuB,EACvB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,GACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,cAAc,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAGpE,YAAY,EACV,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,OAAO,EACP,aAAa,EACb,aAAa,EACb,YAAY,EACZ,WAAW,EACX,aAAa,EACb,cAAc,EACd,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -28,4 +28,8 @@ export { MobileProvider } from './MobileProvider';
|
|
|
28
28
|
export { ResponsiveContainer } from './ResponsiveContainer';
|
|
29
29
|
export { generatePWAManifest } from './pwa';
|
|
30
30
|
export { registerServiceWorker } from './serviceWorker';
|
|
31
|
+
export { createOfflineQueue, IndexedDbOfflineQueue, MemoryOfflineQueue, generateOpId, } from './offlineQueue';
|
|
32
|
+
export { createOfflineDataSource, } from './createOfflineDataSource';
|
|
33
|
+
export { useOfflineSync } from './useOfflineSync';
|
|
34
|
+
export { getServiceWorkerSource, requestBackgroundSync, } from './serviceWorkerSource';
|
|
31
35
|
export { BREAKPOINTS, resolveResponsiveValue } from './breakpoints';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* IndexedDB-backed offline write queue.
|
|
6
|
+
*
|
|
7
|
+
* Designed to back a `createOfflineDataSource()` wrapper: when the network
|
|
8
|
+
* is unavailable the wrapper enqueues mutations here, then replays them in
|
|
9
|
+
* insertion order when connectivity is restored (or a Service Worker
|
|
10
|
+
* `sync` event fires).
|
|
11
|
+
*
|
|
12
|
+
* The implementation is dependency-free and uses raw IndexedDB so it works
|
|
13
|
+
* inside both pages and Service Workers. Falls back to an in-memory store
|
|
14
|
+
* when IndexedDB is unavailable (e.g. during SSR or in unit tests without
|
|
15
|
+
* `fake-indexeddb`).
|
|
16
|
+
*/
|
|
17
|
+
/** A queued mutation. */
|
|
18
|
+
export interface OfflineOperation {
|
|
19
|
+
/** Auto-generated stable id (uuid-like). */
|
|
20
|
+
id: string;
|
|
21
|
+
/** Logical op type. */
|
|
22
|
+
op: 'create' | 'update' | 'delete' | 'custom';
|
|
23
|
+
/** Object/collection the mutation targets. */
|
|
24
|
+
object: string;
|
|
25
|
+
/** Optional record id (required for update/delete). */
|
|
26
|
+
recordId?: string | number;
|
|
27
|
+
/** Mutation payload. */
|
|
28
|
+
payload?: any;
|
|
29
|
+
/**
|
|
30
|
+
* Optional ObjectQL FilterAST snapshot of the *expected* current row
|
|
31
|
+
* state. Used by conflict-resolution to detect "stale write" conflicts
|
|
32
|
+
* when the server row has changed in the interim.
|
|
33
|
+
*
|
|
34
|
+
* The shape is left as `unknown` here so this package doesn't need a
|
|
35
|
+
* runtime dep on `@objectstack/spec` — consumers may pass through any
|
|
36
|
+
* serializable structure.
|
|
37
|
+
*/
|
|
38
|
+
expectFilter?: unknown;
|
|
39
|
+
/** Wall-clock millis when the op was enqueued. */
|
|
40
|
+
enqueuedAt: number;
|
|
41
|
+
/** Number of replay attempts so far. */
|
|
42
|
+
attempts: number;
|
|
43
|
+
/** Last error message, if any. */
|
|
44
|
+
lastError?: string;
|
|
45
|
+
}
|
|
46
|
+
/** Backend-agnostic queue contract. */
|
|
47
|
+
export interface OfflineQueueBackend {
|
|
48
|
+
enqueue(op: OfflineOperation): Promise<void>;
|
|
49
|
+
list(): Promise<OfflineOperation[]>;
|
|
50
|
+
remove(id: string): Promise<void>;
|
|
51
|
+
update(op: OfflineOperation): Promise<void>;
|
|
52
|
+
clear(): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
/** Generate a short, sortable id without external deps. */
|
|
55
|
+
export declare function generateOpId(): string;
|
|
56
|
+
/** IndexedDB-backed queue. Constructor returns a backend usable everywhere. */
|
|
57
|
+
export declare class IndexedDbOfflineQueue implements OfflineQueueBackend {
|
|
58
|
+
private dbPromise;
|
|
59
|
+
private getDb;
|
|
60
|
+
private tx;
|
|
61
|
+
enqueue(op: OfflineOperation): Promise<void>;
|
|
62
|
+
update(op: OfflineOperation): Promise<void>;
|
|
63
|
+
remove(id: string): Promise<void>;
|
|
64
|
+
clear(): Promise<void>;
|
|
65
|
+
list(): Promise<OfflineOperation[]>;
|
|
66
|
+
}
|
|
67
|
+
/** In-memory fallback. Used in SSR / tests / when IndexedDB is blocked. */
|
|
68
|
+
export declare class MemoryOfflineQueue implements OfflineQueueBackend {
|
|
69
|
+
private rows;
|
|
70
|
+
enqueue(op: OfflineOperation): Promise<void>;
|
|
71
|
+
update(op: OfflineOperation): Promise<void>;
|
|
72
|
+
remove(id: string): Promise<void>;
|
|
73
|
+
clear(): Promise<void>;
|
|
74
|
+
list(): Promise<OfflineOperation[]>;
|
|
75
|
+
}
|
|
76
|
+
/** Choose the best available backend for the current runtime. */
|
|
77
|
+
export declare function createOfflineQueue(): OfflineQueueBackend;
|
|
78
|
+
//# sourceMappingURL=offlineQueue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offlineQueue.d.ts","sourceRoot":"","sources":["../src/offlineQueue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,yBAAyB;AACzB,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,EAAE,EAAE,MAAM,CAAC;IACX,uBAAuB;IACvB,EAAE,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC9C,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,wBAAwB;IACxB,OAAO,CAAC,EAAE,GAAG,CAAC;IACd;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,uCAAuC;AACvC,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,IAAI,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,CAAC,EAAE,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,2DAA2D;AAC3D,wBAAgB,YAAY,IAAI,MAAM,CAIrC;AAsBD,+EAA+E;AAC/E,qBAAa,qBAAsB,YAAW,mBAAmB;IAC/D,OAAO,CAAC,SAAS,CAAqC;IACtD,OAAO,CAAC,KAAK;YAIC,EAAE;IAUV,OAAO,CAAC,EAAE,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAC5C,MAAM,CAAC,EAAE,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3C,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IACjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IACtB,IAAI,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;CAW1C;AAED,2EAA2E;AAC3E,qBAAa,kBAAmB,YAAW,mBAAmB;IAC5D,OAAO,CAAC,IAAI,CAAuC;IAC7C,OAAO,CAAC,EAAE,EAAE,gBAAgB;IAC5B,MAAM,CAAC,EAAE,EAAE,gBAAgB;IAC3B,MAAM,CAAC,EAAE,EAAE,MAAM;IACjB,KAAK;IACL,IAAI,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;CAG1C;AAED,iEAAiE;AACjE,wBAAgB,kBAAkB,IAAI,mBAAmB,CAGxD"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* IndexedDB-backed offline write queue.
|
|
6
|
+
*
|
|
7
|
+
* Designed to back a `createOfflineDataSource()` wrapper: when the network
|
|
8
|
+
* is unavailable the wrapper enqueues mutations here, then replays them in
|
|
9
|
+
* insertion order when connectivity is restored (or a Service Worker
|
|
10
|
+
* `sync` event fires).
|
|
11
|
+
*
|
|
12
|
+
* The implementation is dependency-free and uses raw IndexedDB so it works
|
|
13
|
+
* inside both pages and Service Workers. Falls back to an in-memory store
|
|
14
|
+
* when IndexedDB is unavailable (e.g. during SSR or in unit tests without
|
|
15
|
+
* `fake-indexeddb`).
|
|
16
|
+
*/
|
|
17
|
+
/** Generate a short, sortable id without external deps. */
|
|
18
|
+
export function generateOpId() {
|
|
19
|
+
const ts = Date.now().toString(36);
|
|
20
|
+
const rand = Math.random().toString(36).slice(2, 10);
|
|
21
|
+
return `op-${ts}-${rand}`;
|
|
22
|
+
}
|
|
23
|
+
const DB_NAME = 'objectui-offline';
|
|
24
|
+
const STORE = 'queue';
|
|
25
|
+
const DB_VERSION = 1;
|
|
26
|
+
/** Open the IndexedDB database, creating the queue object store on first run. */
|
|
27
|
+
function openDb() {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
30
|
+
req.onupgradeneeded = () => {
|
|
31
|
+
const db = req.result;
|
|
32
|
+
if (!db.objectStoreNames.contains(STORE)) {
|
|
33
|
+
const store = db.createObjectStore(STORE, { keyPath: 'id' });
|
|
34
|
+
store.createIndex('enqueuedAt', 'enqueuedAt', { unique: false });
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
req.onsuccess = () => resolve(req.result);
|
|
38
|
+
req.onerror = () => reject(req.error);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** IndexedDB-backed queue. Constructor returns a backend usable everywhere. */
|
|
42
|
+
export class IndexedDbOfflineQueue {
|
|
43
|
+
constructor() {
|
|
44
|
+
Object.defineProperty(this, "dbPromise", {
|
|
45
|
+
enumerable: true,
|
|
46
|
+
configurable: true,
|
|
47
|
+
writable: true,
|
|
48
|
+
value: null
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
getDb() {
|
|
52
|
+
if (!this.dbPromise)
|
|
53
|
+
this.dbPromise = openDb();
|
|
54
|
+
return this.dbPromise;
|
|
55
|
+
}
|
|
56
|
+
async tx(mode, fn) {
|
|
57
|
+
const db = await this.getDb();
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const tx = db.transaction(STORE, mode);
|
|
60
|
+
const store = tx.objectStore(STORE);
|
|
61
|
+
const req = fn(store);
|
|
62
|
+
req.onsuccess = () => resolve(req.result);
|
|
63
|
+
req.onerror = () => reject(req.error);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async enqueue(op) { await this.tx('readwrite', (s) => s.add(op)); }
|
|
67
|
+
async update(op) { await this.tx('readwrite', (s) => s.put(op)); }
|
|
68
|
+
async remove(id) { await this.tx('readwrite', (s) => s.delete(id)); }
|
|
69
|
+
async clear() { await this.tx('readwrite', (s) => s.clear()); }
|
|
70
|
+
async list() {
|
|
71
|
+
const db = await this.getDb();
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const tx = db.transaction(STORE, 'readonly');
|
|
74
|
+
const store = tx.objectStore(STORE);
|
|
75
|
+
const idx = store.index('enqueuedAt');
|
|
76
|
+
const req = idx.getAll();
|
|
77
|
+
req.onsuccess = () => resolve(req.result ?? []);
|
|
78
|
+
req.onerror = () => reject(req.error);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** In-memory fallback. Used in SSR / tests / when IndexedDB is blocked. */
|
|
83
|
+
export class MemoryOfflineQueue {
|
|
84
|
+
constructor() {
|
|
85
|
+
Object.defineProperty(this, "rows", {
|
|
86
|
+
enumerable: true,
|
|
87
|
+
configurable: true,
|
|
88
|
+
writable: true,
|
|
89
|
+
value: new Map()
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async enqueue(op) { this.rows.set(op.id, op); }
|
|
93
|
+
async update(op) { this.rows.set(op.id, op); }
|
|
94
|
+
async remove(id) { this.rows.delete(id); }
|
|
95
|
+
async clear() { this.rows.clear(); }
|
|
96
|
+
async list() {
|
|
97
|
+
return [...this.rows.values()].sort((a, b) => a.enqueuedAt - b.enqueuedAt);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Choose the best available backend for the current runtime. */
|
|
101
|
+
export function createOfflineQueue() {
|
|
102
|
+
if (typeof indexedDB === 'undefined')
|
|
103
|
+
return new MemoryOfflineQueue();
|
|
104
|
+
try {
|
|
105
|
+
return new IndexedDbOfflineQueue();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return new MemoryOfflineQueue();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* Generates a Service Worker source string with offline support for
|
|
6
|
+
* ObjectUI apps. The output is intended to be written to a file at the
|
|
7
|
+
* web root (e.g. `/service-worker.js`) by the application's build
|
|
8
|
+
* pipeline; it is *not* shipped at runtime by the mobile package itself.
|
|
9
|
+
*
|
|
10
|
+
* The generated SW does three things:
|
|
11
|
+
* 1. Caches the application shell on install (`cache-first` strategy).
|
|
12
|
+
* 2. Falls back to network-first for API calls, so reads are fresh
|
|
13
|
+
* when online and degrade to cached responses when offline.
|
|
14
|
+
* 3. Listens for `sync` events with tag `objectui-offline-queue` and
|
|
15
|
+
* posts a `REPLAY_QUEUE` message to all clients so the running
|
|
16
|
+
* pages can flush their queues from the foreground.
|
|
17
|
+
*/
|
|
18
|
+
export interface ServiceWorkerSourceOptions {
|
|
19
|
+
/** Cache name; bump to invalidate on deploy. */
|
|
20
|
+
cacheName?: string;
|
|
21
|
+
/** Application shell URLs to pre-cache on install. */
|
|
22
|
+
precache?: string[];
|
|
23
|
+
/** URL prefix that identifies API requests (network-first). */
|
|
24
|
+
apiPrefix?: string;
|
|
25
|
+
/** Background-sync tag name. */
|
|
26
|
+
syncTag?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build a Service Worker JavaScript source string. Customize the cache
|
|
30
|
+
* version, app shell, and sync tag via `options`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getServiceWorkerSource(options?: ServiceWorkerSourceOptions): string;
|
|
33
|
+
/**
|
|
34
|
+
* Request a one-shot background sync. The page should call this after
|
|
35
|
+
* enqueuing offline work so the browser will wake the SW when the
|
|
36
|
+
* connection returns. Returns false if Background Sync is unsupported.
|
|
37
|
+
*/
|
|
38
|
+
export declare function requestBackgroundSync(tag?: string): Promise<boolean>;
|
|
39
|
+
//# sourceMappingURL=serviceWorkerSource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serviceWorkerSource.d.ts","sourceRoot":"","sources":["../src/serviceWorkerSource.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,0BAA0B;IACzC,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,0BAA+B,GAAG,MAAM,CA+DvF;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,GAAG,SAA2B,GAAG,OAAO,CAAC,OAAO,CAAC,CAK5F"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* Generates a Service Worker source string with offline support for
|
|
6
|
+
* ObjectUI apps. The output is intended to be written to a file at the
|
|
7
|
+
* web root (e.g. `/service-worker.js`) by the application's build
|
|
8
|
+
* pipeline; it is *not* shipped at runtime by the mobile package itself.
|
|
9
|
+
*
|
|
10
|
+
* The generated SW does three things:
|
|
11
|
+
* 1. Caches the application shell on install (`cache-first` strategy).
|
|
12
|
+
* 2. Falls back to network-first for API calls, so reads are fresh
|
|
13
|
+
* when online and degrade to cached responses when offline.
|
|
14
|
+
* 3. Listens for `sync` events with tag `objectui-offline-queue` and
|
|
15
|
+
* posts a `REPLAY_QUEUE` message to all clients so the running
|
|
16
|
+
* pages can flush their queues from the foreground.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Build a Service Worker JavaScript source string. Customize the cache
|
|
20
|
+
* version, app shell, and sync tag via `options`.
|
|
21
|
+
*/
|
|
22
|
+
export function getServiceWorkerSource(options = {}) {
|
|
23
|
+
const cacheName = options.cacheName ?? 'objectui-shell-v1';
|
|
24
|
+
const precache = options.precache ?? ['/', '/index.html'];
|
|
25
|
+
const apiPrefix = options.apiPrefix ?? '/api/';
|
|
26
|
+
const syncTag = options.syncTag ?? 'objectui-offline-queue';
|
|
27
|
+
return `// Auto-generated by @object-ui/mobile getServiceWorkerSource()
|
|
28
|
+
const CACHE = ${JSON.stringify(cacheName)};
|
|
29
|
+
const PRECACHE = ${JSON.stringify(precache)};
|
|
30
|
+
const API_PREFIX = ${JSON.stringify(apiPrefix)};
|
|
31
|
+
const SYNC_TAG = ${JSON.stringify(syncTag)};
|
|
32
|
+
|
|
33
|
+
self.addEventListener('install', (event) => {
|
|
34
|
+
event.waitUntil(caches.open(CACHE).then((c) => c.addAll(PRECACHE)));
|
|
35
|
+
self.skipWaiting();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
self.addEventListener('activate', (event) => {
|
|
39
|
+
event.waitUntil(
|
|
40
|
+
caches.keys().then((keys) =>
|
|
41
|
+
Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
self.clients.claim();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
self.addEventListener('fetch', (event) => {
|
|
48
|
+
const req = event.request;
|
|
49
|
+
if (req.method !== 'GET') return;
|
|
50
|
+
const url = new URL(req.url);
|
|
51
|
+
|
|
52
|
+
if (url.pathname.startsWith(API_PREFIX)) {
|
|
53
|
+
// Network-first for API
|
|
54
|
+
event.respondWith(
|
|
55
|
+
fetch(req).then((res) => {
|
|
56
|
+
const copy = res.clone();
|
|
57
|
+
caches.open(CACHE).then((c) => c.put(req, copy));
|
|
58
|
+
return res;
|
|
59
|
+
}).catch(() => caches.match(req).then((cached) => cached || Response.error())),
|
|
60
|
+
);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Cache-first for shell assets
|
|
65
|
+
event.respondWith(
|
|
66
|
+
caches.match(req).then((cached) => cached || fetch(req).then((res) => {
|
|
67
|
+
const copy = res.clone();
|
|
68
|
+
caches.open(CACHE).then((c) => c.put(req, copy));
|
|
69
|
+
return res;
|
|
70
|
+
})),
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
self.addEventListener('sync', (event) => {
|
|
75
|
+
if (event.tag !== SYNC_TAG) return;
|
|
76
|
+
event.waitUntil((async () => {
|
|
77
|
+
const all = await self.clients.matchAll({ includeUncontrolled: true });
|
|
78
|
+
for (const client of all) {
|
|
79
|
+
client.postMessage({ type: 'REPLAY_QUEUE', tag: SYNC_TAG });
|
|
80
|
+
}
|
|
81
|
+
})());
|
|
82
|
+
});
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Request a one-shot background sync. The page should call this after
|
|
87
|
+
* enqueuing offline work so the browser will wake the SW when the
|
|
88
|
+
* connection returns. Returns false if Background Sync is unsupported.
|
|
89
|
+
*/
|
|
90
|
+
export async function requestBackgroundSync(tag = 'objectui-offline-queue') {
|
|
91
|
+
if (typeof navigator === 'undefined')
|
|
92
|
+
return false;
|
|
93
|
+
const reg = await navigator.serviceWorker?.ready;
|
|
94
|
+
if (!reg || !reg.sync)
|
|
95
|
+
return false;
|
|
96
|
+
try {
|
|
97
|
+
await reg.sync.register(tag);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* React hook that subscribes to an `OfflineDataSource`, exposing the current
|
|
6
|
+
* online/offline status and the pending queue for status-bar / banner UIs.
|
|
7
|
+
*/
|
|
8
|
+
import type { OfflineDataSource } from './createOfflineDataSource';
|
|
9
|
+
import type { OfflineOperation } from './offlineQueue';
|
|
10
|
+
export interface OfflineSyncState {
|
|
11
|
+
/** True if the browser reports `navigator.onLine`. */
|
|
12
|
+
isOnline: boolean;
|
|
13
|
+
/** Pending ops, oldest first. */
|
|
14
|
+
pending: OfflineOperation[];
|
|
15
|
+
/** True while a replay is in-flight. */
|
|
16
|
+
isReplaying: boolean;
|
|
17
|
+
/** Manually trigger a replay. */
|
|
18
|
+
replay: () => Promise<void>;
|
|
19
|
+
/** Drop a queued op without replaying it. */
|
|
20
|
+
drop: (opId: string) => Promise<void>;
|
|
21
|
+
/** Drop everything. */
|
|
22
|
+
clear: () => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Subscribe to a wrapped offline DataSource. Auto-replays the queue
|
|
26
|
+
* whenever the browser fires `online`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function useOfflineSync(source: OfflineDataSource): OfflineSyncState;
|
|
29
|
+
//# sourceMappingURL=useOfflineSync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useOfflineSync.d.ts","sourceRoot":"","sources":["../src/useOfflineSync.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,MAAM,WAAW,gBAAgB;IAC/B,sDAAsD;IACtD,QAAQ,EAAE,OAAO,CAAC;IAClB,iCAAiC;IACjC,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,wCAAwC;IACxC,WAAW,EAAE,OAAO,CAAC;IACrB,iCAAiC;IACjC,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,6CAA6C;IAC7C,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,uBAAuB;IACvB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,iBAAiB,GAAG,gBAAgB,CA+C1E"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* React hook that subscribes to an `OfflineDataSource`, exposing the current
|
|
6
|
+
* online/offline status and the pending queue for status-bar / banner UIs.
|
|
7
|
+
*/
|
|
8
|
+
import { useEffect, useState, useCallback } from 'react';
|
|
9
|
+
/**
|
|
10
|
+
* Subscribe to a wrapped offline DataSource. Auto-replays the queue
|
|
11
|
+
* whenever the browser fires `online`.
|
|
12
|
+
*/
|
|
13
|
+
export function useOfflineSync(source) {
|
|
14
|
+
const [pending, setPending] = useState([]);
|
|
15
|
+
const [isOnline, setOnline] = useState(() => typeof navigator === 'undefined' ? true : navigator.onLine !== false);
|
|
16
|
+
const [isReplaying, setReplaying] = useState(false);
|
|
17
|
+
const refresh = useCallback(async () => {
|
|
18
|
+
try {
|
|
19
|
+
setPending(await source.pending());
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
setPending([]);
|
|
23
|
+
}
|
|
24
|
+
}, [source]);
|
|
25
|
+
const replay = useCallback(async () => {
|
|
26
|
+
setReplaying(true);
|
|
27
|
+
try {
|
|
28
|
+
await source.replay();
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
setReplaying(false);
|
|
32
|
+
await refresh();
|
|
33
|
+
}
|
|
34
|
+
}, [source, refresh]);
|
|
35
|
+
const drop = useCallback(async (opId) => {
|
|
36
|
+
await source.drop(opId);
|
|
37
|
+
await refresh();
|
|
38
|
+
}, [source, refresh]);
|
|
39
|
+
const clear = useCallback(async () => {
|
|
40
|
+
await source.clear();
|
|
41
|
+
await refresh();
|
|
42
|
+
}, [source, refresh]);
|
|
43
|
+
// Initial load + listen for online/offline transitions.
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
refresh();
|
|
46
|
+
if (typeof window === 'undefined')
|
|
47
|
+
return;
|
|
48
|
+
const onOnline = () => {
|
|
49
|
+
setOnline(true);
|
|
50
|
+
void replay();
|
|
51
|
+
};
|
|
52
|
+
const onOffline = () => setOnline(false);
|
|
53
|
+
window.addEventListener('online', onOnline);
|
|
54
|
+
window.addEventListener('offline', onOffline);
|
|
55
|
+
return () => {
|
|
56
|
+
window.removeEventListener('online', onOnline);
|
|
57
|
+
window.removeEventListener('offline', onOffline);
|
|
58
|
+
};
|
|
59
|
+
}, [refresh, replay]);
|
|
60
|
+
return { isOnline, pending, isReplaying, replay, drop, clear };
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/mobile",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Mobile optimization for Object UI with responsive components, PWA support, and touch gesture handling.",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"react": "^18.0.0 || ^19.0.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@object-ui/types": "3.
|
|
33
|
+
"@object-ui/types": "3.4.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/react": "19.2.14",
|