@baukit/sync-client 0.1.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/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/error.d.ts +20 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +31 -0
- package/dist/error.js.map +1 -0
- package/dist/expo.d.ts +12 -0
- package/dist/expo.d.ts.map +1 -0
- package/dist/expo.js +75 -0
- package/dist/expo.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/push-batch.d.ts +57 -0
- package/dist/push-batch.d.ts.map +1 -0
- package/dist/push-batch.js +39 -0
- package/dist/push-batch.js.map +1 -0
- package/dist/scheduler.d.ts +52 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +116 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/store.d.ts +87 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +156 -0
- package/dist/store.js.map +1 -0
- package/dist/transport.d.ts +80 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +131 -0
- package/dist/transport.js.map +1 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patrick Koss
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# @baukit/sync-client
|
|
2
|
+
|
|
3
|
+
`@baukit/sync-client` holds the four client-side pieces every offline-capable product rebuilds
|
|
4
|
+
around a sync loop: when to run, how to send a request, how to report status, and what order to
|
|
5
|
+
push a batch in. It does not define a sync protocol. Endpoint paths, payloads, conflict rules, and
|
|
6
|
+
the sync engine itself stay product-owned, as
|
|
7
|
+
[the offline readiness contract](../../../docs/platform/offline-readiness-contract.md) says they
|
|
8
|
+
must.
|
|
9
|
+
|
|
10
|
+
The root entry has no runtime dependencies and no React. Timers, foreground state, connectivity,
|
|
11
|
+
and `fetch` all arrive by injection, so the same code runs under Node, a browser, and React Native.
|
|
12
|
+
The optional `@baukit/sync-client/expo` entry imports `react-native` and `expo-network`; neither is
|
|
13
|
+
loaded by the root entry.
|
|
14
|
+
|
|
15
|
+
## Scheduler
|
|
16
|
+
|
|
17
|
+
`SyncScheduler` takes one opaque `run()` callback and a `SyncSchedulerEnvironment`. It guarantees
|
|
18
|
+
a single active run: a trigger arriving mid-run joins that run instead of starting a second one.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
const scheduler = new SyncScheduler(() => engine.run(), environment, { intervalMs: 60_000 });
|
|
22
|
+
scheduler.start();
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`trigger()` starts a run or joins the active one. `requestFollowUp()` is what a local write calls:
|
|
26
|
+
if a run is already active it queues exactly one replay after that run settles, so a write made
|
|
27
|
+
while the engine was mid-push is never left unsent. Repeated follow-up requests still collapse
|
|
28
|
+
into one replay. `stop()` drops the queued replay along with the interval and subscriptions.
|
|
29
|
+
|
|
30
|
+
The environment reports foreground state and connectivity. The scheduler runs on start, when the
|
|
31
|
+
app returns to the foreground, when connectivity returns, and on the interval. It installs no
|
|
32
|
+
interval while backgrounded and ignores connectivity events there.
|
|
33
|
+
|
|
34
|
+
A failing run reaches `onError` and does not stop scheduling. Backoff between attempts belongs to
|
|
35
|
+
the product's engine, inside `run()`, where it can see which failures are retryable.
|
|
36
|
+
|
|
37
|
+
### Expo environment
|
|
38
|
+
|
|
39
|
+
Expo products can use the supplied environment instead of repeating AppState and network wiring:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { SyncScheduler } from '@baukit/sync-client';
|
|
43
|
+
import { createExpoSyncEnvironment } from '@baukit/sync-client/expo';
|
|
44
|
+
|
|
45
|
+
const scheduler = new SyncScheduler(() => engine.run(), createExpoSyncEnvironment());
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The environment treats only AppState `active` as foreground. On native platforms it reads the
|
|
49
|
+
initial state from `expo-network` and triggers only when connectivity changes from unusable to
|
|
50
|
+
usable. Expo web uses the browser `online` event. Tests can pass `{ timers }` to replace
|
|
51
|
+
`setInterval` and `clearInterval`. `react-native` and `expo-network` are optional peers because
|
|
52
|
+
non-Expo consumers never import this subpath.
|
|
53
|
+
|
|
54
|
+
## Transport
|
|
55
|
+
|
|
56
|
+
The original `SyncTransport` constructor wraps an injected `fetch` and auth header provider, and
|
|
57
|
+
turns every failure into a typed error:
|
|
58
|
+
|
|
59
|
+
- `SyncAuthError` for 401, never retryable;
|
|
60
|
+
- `SyncPartitionMismatchError` when the body carries the configured partition-mismatch code,
|
|
61
|
+
meaning the local database belongs to an erased or replaced owner;
|
|
62
|
+
- `SyncTransportError` otherwise, with `retryable` true for 408, 429, and 5xx, plus network and
|
|
63
|
+
body-read failures.
|
|
64
|
+
|
|
65
|
+
Auth headers resolve per attempt, so a token refreshed between retries is picked up. The
|
|
66
|
+
partition header is sent only when the caller passes a `partitionId`, and both its name and the
|
|
67
|
+
mismatch code are configurable. The transport never names an endpoint: callers pass the path and
|
|
68
|
+
the response type.
|
|
69
|
+
|
|
70
|
+
A product API client often already owns the base URL, token refresh, authentication recovery,
|
|
71
|
+
response decoding, and its error policy. Pass its decoded request function instead of duplicating
|
|
72
|
+
that work in the transport:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const transport = new SyncTransport({
|
|
76
|
+
request: apiClient.request.bind(apiClient),
|
|
77
|
+
partitionHeader: 'X-Product-Profile-Id',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await transport.request('/api/v1/sync/push', {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
body: { changes },
|
|
83
|
+
partitionId: profileId,
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
This mode builds the relative path, query string, JSON body, and partition header, then delegates.
|
|
88
|
+
The request function owns decoding and errors, which keeps the product API client's existing auth
|
|
89
|
+
recovery intact. The fetch-backed constructor remains unchanged for products that want
|
|
90
|
+
`SyncTransportError` classification from response status and body.
|
|
91
|
+
|
|
92
|
+
## Status store
|
|
93
|
+
|
|
94
|
+
`SyncStatusStore` is the observable sync status, with `subscribe`/`getSnapshot` and no state
|
|
95
|
+
library. It encodes rules the offline readiness contract requires and products keep getting
|
|
96
|
+
wrong:
|
|
97
|
+
|
|
98
|
+
- an actionable rejection outranks a transport failure, so a failed run over existing conflicts
|
|
99
|
+
still reads `attention`;
|
|
100
|
+
- unsent work outranks a bare error, so a failed run with pending changes reads `pending`,
|
|
101
|
+
not `error`;
|
|
102
|
+
- the first run is the initial pull; later runs are settled activity.
|
|
103
|
+
|
|
104
|
+
`refreshRevision` increments whenever a run delivers a new authoritative snapshot.
|
|
105
|
+
`deriveLocalStoreReadiness` compares it against what a consumer has actually rendered, which is
|
|
106
|
+
how a cold start avoids flashing an empty state over data that is still arriving.
|
|
107
|
+
`deriveInitialSyncState` maps a snapshot onto the contract's `unknown` / `syncing` /
|
|
108
|
+
`offline-cached` / `settled` vocabulary.
|
|
109
|
+
|
|
110
|
+
The store is generic over its attention item and defaults to the existing
|
|
111
|
+
`{ entityType, entityId }` shape. This keeps one store API while allowing a product to retain the
|
|
112
|
+
rejection details its UI needs:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
interface ProductAttention {
|
|
116
|
+
object_entity_type: string;
|
|
117
|
+
object_entity_id: string;
|
|
118
|
+
rejections: readonly Rejection[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const status = new SyncStatusStore<ProductAttention>();
|
|
122
|
+
status.setAttention(items, pendingCount);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
For stores and screens that use API-style names, `toSnakeCaseSnapshot` returns a typed projection:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const snapshot = toSnakeCaseSnapshot(status.getSnapshot());
|
|
129
|
+
|
|
130
|
+
snapshot.last_sync_at;
|
|
131
|
+
snapshot.pending_count;
|
|
132
|
+
snapshot.initial_pull_status;
|
|
133
|
+
snapshot.refresh_revision;
|
|
134
|
+
snapshot.security_block;
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`status`, `error`, and `attention` keep their names. Attention items also keep their original
|
|
138
|
+
shape.
|
|
139
|
+
|
|
140
|
+
## Push batch ranking
|
|
141
|
+
|
|
142
|
+
`rankPushBatch` orders one push batch so a parent is never sent after its children.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const rank = dependencyRankByOrder(['container', 'group', 'leaf']);
|
|
146
|
+
const batch = rankPushBatch(pending, {
|
|
147
|
+
rank,
|
|
148
|
+
batchSize: 200,
|
|
149
|
+
isHeldBack: (change) => hasUnsentChildren(change),
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Changes are grouped by `entityType:entityId`. Each group keeps the position of its first
|
|
154
|
+
occurrence, so foreign-key order survives coalescing, and carries the newest change so the
|
|
155
|
+
request sends current data. `coveredChangeIds` lists every queued change the group settles,
|
|
156
|
+
because server outcomes are keyed by entity rather than by outbox row: one outcome clears all of
|
|
157
|
+
them.
|
|
158
|
+
|
|
159
|
+
`isHeldBack` is the other half of the rule. A parent whose required children are still unsent is
|
|
160
|
+
dropped from the batch rather than reordered, and retried on a later run once those children have
|
|
161
|
+
been accepted. Sending it now would show the server a complete parent while a required child is
|
|
162
|
+
missing. Held-back entities do not consume batch-size slots.
|
|
163
|
+
|
|
164
|
+
Entity names, the dependency order, and the hold-back predicate are all product inputs. Baukit
|
|
165
|
+
knows only that a rank exists.
|
package/dist/error.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** A sync request failed. `retryable` decides whether a later attempt may win. */
|
|
2
|
+
export declare class SyncTransportError extends Error {
|
|
3
|
+
readonly retryable: boolean;
|
|
4
|
+
readonly cause?: unknown | undefined;
|
|
5
|
+
constructor(message: string, retryable: boolean, cause?: unknown | undefined);
|
|
6
|
+
}
|
|
7
|
+
/** Credentials were rejected. Never retryable without re-authentication. */
|
|
8
|
+
export declare class SyncAuthError extends SyncTransportError {
|
|
9
|
+
readonly kind = "auth";
|
|
10
|
+
constructor(message: string, cause?: unknown);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The server rejected the caller's local-data partition, so the local database
|
|
14
|
+
* belongs to an erased or replaced owner and must not be reused.
|
|
15
|
+
*/
|
|
16
|
+
export declare class SyncPartitionMismatchError extends SyncTransportError {
|
|
17
|
+
readonly kind = "partition-mismatch";
|
|
18
|
+
constructor(message: string, cause?: unknown);
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,qBAAa,kBAAmB,SAAQ,KAAK;IAGzC,QAAQ,CAAC,SAAS,EAAE,OAAO;aACT,KAAK,CAAC,EAAE,OAAO;gBAFjC,OAAO,EAAE,MAAM,EACN,SAAS,EAAE,OAAO,EACT,KAAK,CAAC,EAAE,OAAO,YAAA;CAKpC;AAED,4EAA4E;AAC5E,qBAAa,aAAc,SAAQ,kBAAkB;IACnD,QAAQ,CAAC,IAAI,UAAU;gBAEX,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI7C;AAED;;;GAGG;AACH,qBAAa,0BAA2B,SAAQ,kBAAkB;IAChE,QAAQ,CAAC,IAAI,wBAAwB;gBAEzB,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI7C"}
|
package/dist/error.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** A sync request failed. `retryable` decides whether a later attempt may win. */
|
|
2
|
+
export class SyncTransportError extends Error {
|
|
3
|
+
retryable;
|
|
4
|
+
cause;
|
|
5
|
+
constructor(message, retryable, cause) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.retryable = retryable;
|
|
8
|
+
this.cause = cause;
|
|
9
|
+
this.name = 'SyncTransportError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Credentials were rejected. Never retryable without re-authentication. */
|
|
13
|
+
export class SyncAuthError extends SyncTransportError {
|
|
14
|
+
kind = 'auth';
|
|
15
|
+
constructor(message, cause) {
|
|
16
|
+
super(message, false, cause);
|
|
17
|
+
this.name = 'SyncAuthError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The server rejected the caller's local-data partition, so the local database
|
|
22
|
+
* belongs to an erased or replaced owner and must not be reused.
|
|
23
|
+
*/
|
|
24
|
+
export class SyncPartitionMismatchError extends SyncTransportError {
|
|
25
|
+
kind = 'partition-mismatch';
|
|
26
|
+
constructor(message, cause) {
|
|
27
|
+
super(message, false, cause);
|
|
28
|
+
this.name = 'SyncPartitionMismatchError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.js","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAGhC;IACS;IAHpB,YACE,OAAe,EACN,SAAkB,EACT,KAAe;QAEjC,KAAK,CAAC,OAAO,CAAC,CAAC;QAHN,cAAS,GAAT,SAAS,CAAS;QACT,UAAK,GAAL,KAAK,CAAU;QAGjC,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED,4EAA4E;AAC5E,MAAM,OAAO,aAAc,SAAQ,kBAAkB;IAC1C,IAAI,GAAG,MAAM,CAAC;IAEvB,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC9B,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,0BAA2B,SAAQ,kBAAkB;IACvD,IAAI,GAAG,oBAAoB,CAAC;IAErC,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAC;IAC3C,CAAC;CACF"}
|
package/dist/expo.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SyncSchedulerEnvironment, SyncSchedulerTimer } from './scheduler.js';
|
|
2
|
+
export interface ExpoSyncTimers {
|
|
3
|
+
setInterval(callback: () => void, milliseconds: number): SyncSchedulerTimer;
|
|
4
|
+
clearInterval(handle: SyncSchedulerTimer): void;
|
|
5
|
+
}
|
|
6
|
+
export interface ExpoSyncEnvironmentOptions {
|
|
7
|
+
/** Overrides global timers, primarily for deterministic scheduler tests. */
|
|
8
|
+
timers?: ExpoSyncTimers;
|
|
9
|
+
}
|
|
10
|
+
/** Creates the Expo runtime environment consumed by {@link SyncScheduler}. */
|
|
11
|
+
export declare function createExpoSyncEnvironment(options?: ExpoSyncEnvironmentOptions): SyncSchedulerEnvironment;
|
|
12
|
+
//# sourceMappingURL=expo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expo.d.ts","sourceRoot":"","sources":["../src/expo.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEnF,MAAM,WAAW,cAAc;IAC7B,WAAW,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,YAAY,EAAE,MAAM,GAAG,kBAAkB,CAAC;IAC5E,aAAa,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACjD;AAED,MAAM,WAAW,0BAA0B;IACzC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AA4DD,8EAA8E;AAC9E,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,0BAA+B,GACvC,wBAAwB,CAwB1B"}
|
package/dist/expo.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as Network from 'expo-network';
|
|
2
|
+
import { AppState, Platform } from 'react-native';
|
|
3
|
+
const defaultTimers = {
|
|
4
|
+
setInterval: (callback, milliseconds) => globalThis.setInterval(callback, milliseconds),
|
|
5
|
+
clearInterval: (handle) => {
|
|
6
|
+
globalThis.clearInterval(handle);
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
function isUsableNetwork(state) {
|
|
10
|
+
return state.isConnected === true && state.isInternetReachable !== false;
|
|
11
|
+
}
|
|
12
|
+
function subscribeBrowserOnline(listener) {
|
|
13
|
+
if (typeof window === 'undefined') {
|
|
14
|
+
return () => undefined;
|
|
15
|
+
}
|
|
16
|
+
window.addEventListener('online', listener);
|
|
17
|
+
return () => {
|
|
18
|
+
window.removeEventListener('online', listener);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function subscribeNativeOnline(listener) {
|
|
22
|
+
const network = Network;
|
|
23
|
+
let subscribed = true;
|
|
24
|
+
let receivedEvent = false;
|
|
25
|
+
let wasUsable = null;
|
|
26
|
+
void network
|
|
27
|
+
.getNetworkStateAsync()
|
|
28
|
+
.then((state) => {
|
|
29
|
+
if (subscribed && !receivedEvent) {
|
|
30
|
+
wasUsable = isUsableNetwork(state);
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
.catch(() => undefined);
|
|
34
|
+
const subscription = network.addNetworkStateListener?.((state) => {
|
|
35
|
+
if (!subscribed)
|
|
36
|
+
return;
|
|
37
|
+
receivedEvent = true;
|
|
38
|
+
const usable = isUsableNetwork(state);
|
|
39
|
+
if (wasUsable === false && usable) {
|
|
40
|
+
listener();
|
|
41
|
+
}
|
|
42
|
+
wasUsable = usable;
|
|
43
|
+
});
|
|
44
|
+
return () => {
|
|
45
|
+
subscribed = false;
|
|
46
|
+
subscription?.remove();
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Creates the Expo runtime environment consumed by {@link SyncScheduler}. */
|
|
50
|
+
export function createExpoSyncEnvironment(options = {}) {
|
|
51
|
+
const timers = options.timers ?? defaultTimers;
|
|
52
|
+
return {
|
|
53
|
+
isActive: () => AppState.currentState === 'active',
|
|
54
|
+
subscribeActive(listener) {
|
|
55
|
+
const subscription = AppState.addEventListener('change', (state) => {
|
|
56
|
+
listener(state === 'active');
|
|
57
|
+
});
|
|
58
|
+
return () => {
|
|
59
|
+
subscription.remove();
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
subscribeOnline(listener) {
|
|
63
|
+
return Platform.OS === 'web'
|
|
64
|
+
? subscribeBrowserOnline(listener)
|
|
65
|
+
: subscribeNativeOnline(listener);
|
|
66
|
+
},
|
|
67
|
+
setInterval(callback, milliseconds) {
|
|
68
|
+
return timers.setInterval(callback, milliseconds);
|
|
69
|
+
},
|
|
70
|
+
clearInterval(handle) {
|
|
71
|
+
timers.clearInterval(handle);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=expo.js.map
|
package/dist/expo.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expo.js","sourceRoot":"","sources":["../src/expo.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { SyncAuthError, SyncPartitionMismatchError, SyncTransportError } from './error.js';
|
|
2
|
+
export { dependencyRankByOrder, rankPushBatch } from './push-batch.js';
|
|
3
|
+
export type { PushCandidate, RankedPushItem, RankPushBatchOptions } from './push-batch.js';
|
|
4
|
+
export { SyncScheduler } from './scheduler.js';
|
|
5
|
+
export type { SyncSchedulerEnvironment, SyncSchedulerOptions, SyncSchedulerTimer, } from './scheduler.js';
|
|
6
|
+
export { deriveInitialSyncState, deriveLocalStoreReadiness, SyncStatusStore, toSnakeCaseSnapshot, } from './store.js';
|
|
7
|
+
export type { InitialPullStatus, InitialSyncState, LocalStoreReadiness, LocalStoreReadinessInput, SnakeCaseSyncStatusSnapshot, SyncAttentionItem, SyncStatus, SyncStatusSink, SyncStatusSnapshot, } from './store.js';
|
|
8
|
+
export { SyncTransport } from './transport.js';
|
|
9
|
+
export type { SyncFetch, SyncFetchResponse, SyncPrebuiltRequest, SyncPrebuiltRequestTransportOptions, SyncRequestOptions, SyncRequestInit, SyncFetchTransportOptions, SyncTransportOptions, } from './transport.js';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC3F,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACvE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC3F,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EACf,mBAAmB,GACpB,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,EACnB,wBAAwB,EACxB,2BAA2B,EAC3B,iBAAiB,EACjB,UAAU,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,mBAAmB,EACnB,mCAAmC,EACnC,kBAAkB,EAClB,eAAe,EACf,yBAAyB,EACzB,oBAAoB,GACrB,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { SyncAuthError, SyncPartitionMismatchError, SyncTransportError } from './error.js';
|
|
2
|
+
export { dependencyRankByOrder, rankPushBatch } from './push-batch.js';
|
|
3
|
+
export { SyncScheduler } from './scheduler.js';
|
|
4
|
+
export { deriveInitialSyncState, deriveLocalStoreReadiness, SyncStatusStore, toSnakeCaseSnapshot, } from './store.js';
|
|
5
|
+
export { SyncTransport } from './transport.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC3F,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAM/C,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EACf,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAYpB,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One unsent local change, as baukit sees it.
|
|
3
|
+
*
|
|
4
|
+
* `entityType` and `entityId` identify the row the change targets. Baukit never
|
|
5
|
+
* interprets either: entity names, payloads, and operations stay product-owned.
|
|
6
|
+
*/
|
|
7
|
+
export interface PushCandidate {
|
|
8
|
+
/** Stable identity of this queued change, unique across the outbox. */
|
|
9
|
+
changeId: string;
|
|
10
|
+
entityType: string;
|
|
11
|
+
entityId: string;
|
|
12
|
+
}
|
|
13
|
+
/** One entity's place in the batch, after coalescing and ordering. */
|
|
14
|
+
export interface RankedPushItem<T extends PushCandidate> {
|
|
15
|
+
/** The newest queued change for this entity; its payload is the one to send. */
|
|
16
|
+
change: T;
|
|
17
|
+
/**
|
|
18
|
+
* Every queued change this item settles, oldest first. One server outcome
|
|
19
|
+
* clears all of them, because outcomes are keyed by entity, not by change.
|
|
20
|
+
*/
|
|
21
|
+
coveredChangeIds: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface RankPushBatchOptions<T extends PushCandidate> {
|
|
24
|
+
/**
|
|
25
|
+
* Dependency rank of an entity type. Lower ranks are sent first, so a parent
|
|
26
|
+
* type must rank below every type that references it. Unknown types sort last.
|
|
27
|
+
*/
|
|
28
|
+
rank: (entityType: string) => number;
|
|
29
|
+
/** Largest number of entities one request may carry. */
|
|
30
|
+
batchSize: number;
|
|
31
|
+
/**
|
|
32
|
+
* Entities whose required children are still unsent, or otherwise not yet
|
|
33
|
+
* safe to send. Held-back entities are dropped from the batch and retried on
|
|
34
|
+
* a later run, once their children have been accepted.
|
|
35
|
+
*/
|
|
36
|
+
isHeldBack?: (change: T) => boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Orders a push batch parent-before-child and coalesces per entity.
|
|
40
|
+
*
|
|
41
|
+
* Queued changes are grouped by `entityType:entityId`. Each group keeps the
|
|
42
|
+
* position of its first occurrence, so foreign-key order survives coalescing,
|
|
43
|
+
* and carries the newest change so the request sends current data. Groups are
|
|
44
|
+
* then sorted by dependency rank, ties broken by that first position, and the
|
|
45
|
+
* batch is truncated to `batchSize`.
|
|
46
|
+
*
|
|
47
|
+
* A parent whose children are unsent is held back rather than reordered: the
|
|
48
|
+
* server would otherwise see a complete parent while a required child is still
|
|
49
|
+
* missing.
|
|
50
|
+
*/
|
|
51
|
+
export declare function rankPushBatch<T extends PushCandidate>(pending: readonly T[], { rank, batchSize, isHeldBack }: RankPushBatchOptions<T>): RankedPushItem<T>[];
|
|
52
|
+
/**
|
|
53
|
+
* Builds a {@link RankPushBatchOptions.rank} function from an ordered list of
|
|
54
|
+
* entity types. Types absent from the list sort after every listed type.
|
|
55
|
+
*/
|
|
56
|
+
export declare function dependencyRankByOrder(order: readonly string[]): (entityType: string) => number;
|
|
57
|
+
//# sourceMappingURL=push-batch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-batch.d.ts","sourceRoot":"","sources":["../src/push-batch.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,sEAAsE;AACtE,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,aAAa;IACrD,gFAAgF;IAChF,MAAM,EAAE,CAAC,CAAC;IACV;;;OAGG;IACH,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,oBAAoB,CAAC,CAAC,SAAS,aAAa;IAC3D;;;OAGG;IACH,IAAI,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IACrC,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC;CACrC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,aAAa,EACnD,OAAO,EAAE,SAAS,CAAC,EAAE,EACrB,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE,oBAAoB,CAAC,CAAC,CAAC,GACvD,cAAc,CAAC,CAAC,CAAC,EAAE,CAqBrB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAG9F"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orders a push batch parent-before-child and coalesces per entity.
|
|
3
|
+
*
|
|
4
|
+
* Queued changes are grouped by `entityType:entityId`. Each group keeps the
|
|
5
|
+
* position of its first occurrence, so foreign-key order survives coalescing,
|
|
6
|
+
* and carries the newest change so the request sends current data. Groups are
|
|
7
|
+
* then sorted by dependency rank, ties broken by that first position, and the
|
|
8
|
+
* batch is truncated to `batchSize`.
|
|
9
|
+
*
|
|
10
|
+
* A parent whose children are unsent is held back rather than reordered: the
|
|
11
|
+
* server would otherwise see a complete parent while a required child is still
|
|
12
|
+
* missing.
|
|
13
|
+
*/
|
|
14
|
+
export function rankPushBatch(pending, { rank, batchSize, isHeldBack }) {
|
|
15
|
+
const grouped = new Map();
|
|
16
|
+
pending.forEach((change, order) => {
|
|
17
|
+
const key = `${change.entityType}:${change.entityId}`;
|
|
18
|
+
const existing = grouped.get(key);
|
|
19
|
+
grouped.set(key, {
|
|
20
|
+
change,
|
|
21
|
+
coveredChangeIds: [...(existing?.coveredChangeIds ?? []), change.changeId],
|
|
22
|
+
order: existing?.order ?? order,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
return [...grouped.values()]
|
|
26
|
+
.filter(({ change }) => !isHeldBack?.(change))
|
|
27
|
+
.sort((left, right) => rank(left.change.entityType) - rank(right.change.entityType) || left.order - right.order)
|
|
28
|
+
.slice(0, batchSize)
|
|
29
|
+
.map(({ change, coveredChangeIds }) => ({ change, coveredChangeIds }));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Builds a {@link RankPushBatchOptions.rank} function from an ordered list of
|
|
33
|
+
* entity types. Types absent from the list sort after every listed type.
|
|
34
|
+
*/
|
|
35
|
+
export function dependencyRankByOrder(order) {
|
|
36
|
+
const ranks = new Map(order.map((entityType, index) => [entityType, index]));
|
|
37
|
+
return (entityType) => ranks.get(entityType) ?? order.length;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=push-batch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-batch.js","sourceRoot":"","sources":["../src/push-batch.ts"],"names":[],"mappings":"AAwCA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAqB,EACrB,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAA2B;IAExD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAoE,CAAC;IAE5F,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;QAChC,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,UAAU,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACtD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE;YACf,MAAM;YACN,gBAAgB,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,gBAAgB,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;YAC1E,KAAK,EAAE,QAAQ,EAAE,KAAK,IAAI,KAAK;SAChC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;SACzB,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC,CAAC;SAC7C,IAAI,CACH,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CACd,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAC3F;SACA,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC;SACnB,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC;AAC3E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAwB;IAC5D,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7E,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC;AAC/D,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host capabilities the scheduler needs. Products supply timers, foreground
|
|
3
|
+
* state, and connectivity; baukit never reaches for a global.
|
|
4
|
+
*/
|
|
5
|
+
export interface SyncSchedulerEnvironment {
|
|
6
|
+
isActive(): boolean;
|
|
7
|
+
subscribeActive(listener: (active: boolean) => void): () => void;
|
|
8
|
+
subscribeOnline(listener: () => void): () => void;
|
|
9
|
+
setInterval(callback: () => void, milliseconds: number): SyncSchedulerTimer;
|
|
10
|
+
clearInterval(handle: SyncSchedulerTimer): void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Opaque timer handle returned by the host's `setInterval`. Baukit only stores
|
|
14
|
+
* it and hands it back to `clearInterval`; its runtime shape is host-defined.
|
|
15
|
+
*/
|
|
16
|
+
export type SyncSchedulerTimer = {
|
|
17
|
+
readonly __syncSchedulerTimer?: never;
|
|
18
|
+
} & object;
|
|
19
|
+
export interface SyncSchedulerOptions {
|
|
20
|
+
intervalMs?: number;
|
|
21
|
+
onError?: (error: unknown) => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Runs one opaque sync callback at most once at a time.
|
|
25
|
+
*
|
|
26
|
+
* A trigger that arrives while a run is active joins that run. A follow-up
|
|
27
|
+
* requested during a run replays the callback once after it settles, so writes
|
|
28
|
+
* made mid-run are never left unsent without starting a parallel run.
|
|
29
|
+
*/
|
|
30
|
+
export declare class SyncScheduler {
|
|
31
|
+
private readonly run;
|
|
32
|
+
private readonly environment;
|
|
33
|
+
private readonly intervalMs;
|
|
34
|
+
private readonly onError;
|
|
35
|
+
private active;
|
|
36
|
+
private started;
|
|
37
|
+
private inFlight;
|
|
38
|
+
private rerunRequested;
|
|
39
|
+
private interval;
|
|
40
|
+
private subscriptions;
|
|
41
|
+
constructor(run: () => Promise<unknown>, environment: SyncSchedulerEnvironment, options?: SyncSchedulerOptions);
|
|
42
|
+
start(): void;
|
|
43
|
+
stop(): void;
|
|
44
|
+
/** Starts a run, or joins the active one. */
|
|
45
|
+
trigger(): Promise<void>;
|
|
46
|
+
/** Queues exactly one more run when a run is already active. */
|
|
47
|
+
requestFollowUp(): Promise<void>;
|
|
48
|
+
private runUntilSettled;
|
|
49
|
+
private refreshInterval;
|
|
50
|
+
private stopInterval;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACvC,QAAQ,IAAI,OAAO,CAAC;IACpB,eAAe,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACjE,eAAe,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAClD,WAAW,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,YAAY,EAAE,MAAM,GAAG,kBAAkB,CAAC;IAC5E,aAAa,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACjD;AAED;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAAE,QAAQ,CAAC,oBAAoB,CAAC,EAAE,KAAK,CAAA;CAAE,GAAG,MAAM,CAAC;AAEpF,MAAM,WAAW,oBAAoB;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAQD;;;;;;GAMG;AACH,qBAAa,aAAa;IAWtB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAX9B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;IACnD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,aAAa,CAAsB;gBAGxB,GAAG,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,EAC3B,WAAW,EAAE,wBAAwB,EACtD,OAAO,GAAE,oBAAyB;IAMpC,KAAK,IAAI,IAAI;IA0Bb,IAAI,IAAI,IAAI;IAUZ,6CAA6C;IAC7C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAexB,gEAAgE;IAChE,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;YAQlB,eAAe;IAa7B,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,YAAY;CAOrB"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const DEFAULT_INTERVAL_MS = 5 * 60 * 1000;
|
|
2
|
+
function noop() {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Runs one opaque sync callback at most once at a time.
|
|
7
|
+
*
|
|
8
|
+
* A trigger that arrives while a run is active joins that run. A follow-up
|
|
9
|
+
* requested during a run replays the callback once after it settles, so writes
|
|
10
|
+
* made mid-run are never left unsent without starting a parallel run.
|
|
11
|
+
*/
|
|
12
|
+
export class SyncScheduler {
|
|
13
|
+
run;
|
|
14
|
+
environment;
|
|
15
|
+
intervalMs;
|
|
16
|
+
onError;
|
|
17
|
+
active = false;
|
|
18
|
+
started = false;
|
|
19
|
+
inFlight = null;
|
|
20
|
+
rerunRequested = false;
|
|
21
|
+
interval = null;
|
|
22
|
+
subscriptions = [];
|
|
23
|
+
constructor(run, environment, options = {}) {
|
|
24
|
+
this.run = run;
|
|
25
|
+
this.environment = environment;
|
|
26
|
+
this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
27
|
+
this.onError = options.onError ?? noop;
|
|
28
|
+
}
|
|
29
|
+
start() {
|
|
30
|
+
if (this.started) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.started = true;
|
|
34
|
+
this.active = this.environment.isActive();
|
|
35
|
+
this.subscriptions = [
|
|
36
|
+
this.environment.subscribeActive((active) => {
|
|
37
|
+
this.active = active;
|
|
38
|
+
this.refreshInterval();
|
|
39
|
+
if (active) {
|
|
40
|
+
void this.trigger();
|
|
41
|
+
}
|
|
42
|
+
}),
|
|
43
|
+
this.environment.subscribeOnline(() => {
|
|
44
|
+
if (this.active) {
|
|
45
|
+
void this.trigger();
|
|
46
|
+
}
|
|
47
|
+
}),
|
|
48
|
+
];
|
|
49
|
+
this.refreshInterval();
|
|
50
|
+
if (this.active) {
|
|
51
|
+
void this.trigger();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
stop() {
|
|
55
|
+
this.started = false;
|
|
56
|
+
this.rerunRequested = false;
|
|
57
|
+
this.stopInterval();
|
|
58
|
+
this.subscriptions.forEach((unsubscribe) => {
|
|
59
|
+
unsubscribe();
|
|
60
|
+
});
|
|
61
|
+
this.subscriptions = [];
|
|
62
|
+
}
|
|
63
|
+
/** Starts a run, or joins the active one. */
|
|
64
|
+
trigger() {
|
|
65
|
+
if (this.inFlight) {
|
|
66
|
+
return this.inFlight;
|
|
67
|
+
}
|
|
68
|
+
const task = Promise.resolve()
|
|
69
|
+
.then(() => this.runUntilSettled())
|
|
70
|
+
.finally(() => {
|
|
71
|
+
if (this.inFlight === task) {
|
|
72
|
+
this.inFlight = null;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
this.inFlight = task;
|
|
76
|
+
return task;
|
|
77
|
+
}
|
|
78
|
+
/** Queues exactly one more run when a run is already active. */
|
|
79
|
+
requestFollowUp() {
|
|
80
|
+
if (this.inFlight) {
|
|
81
|
+
this.rerunRequested = true;
|
|
82
|
+
return this.inFlight;
|
|
83
|
+
}
|
|
84
|
+
return this.trigger();
|
|
85
|
+
}
|
|
86
|
+
async runUntilSettled() {
|
|
87
|
+
let rerun = true;
|
|
88
|
+
while (rerun) {
|
|
89
|
+
this.rerunRequested = false;
|
|
90
|
+
try {
|
|
91
|
+
await this.run();
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
this.onError(error);
|
|
95
|
+
}
|
|
96
|
+
rerun = this.rerunRequested;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
refreshInterval() {
|
|
100
|
+
this.stopInterval();
|
|
101
|
+
if (!this.started || !this.active) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.interval = this.environment.setInterval(() => {
|
|
105
|
+
void this.trigger();
|
|
106
|
+
}, this.intervalMs);
|
|
107
|
+
}
|
|
108
|
+
stopInterval() {
|
|
109
|
+
if (this.interval === null) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.environment.clearInterval(this.interval);
|
|
113
|
+
this.interval = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAuBA,MAAM,mBAAmB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAE1C,SAAS,IAAI;IACX,OAAO;AACT,CAAC;AAED;;;;;;GAMG;AACH,MAAM,OAAO,aAAa;IAWL;IACA;IAXF,UAAU,CAAS;IACnB,OAAO,CAA2B;IAC3C,MAAM,GAAG,KAAK,CAAC;IACf,OAAO,GAAG,KAAK,CAAC;IAChB,QAAQ,GAAyB,IAAI,CAAC;IACtC,cAAc,GAAG,KAAK,CAAC;IACvB,QAAQ,GAA8B,IAAI,CAAC;IAC3C,aAAa,GAAmB,EAAE,CAAC;IAE3C,YACmB,GAA2B,EAC3B,WAAqC,EACtD,UAAgC,EAAE;QAFjB,QAAG,GAAH,GAAG,CAAwB;QAC3B,gBAAW,GAAX,WAAW,CAA0B;QAGtD,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,mBAAmB,CAAC;QAC5D,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,IAAI,CAAC;IACzC,CAAC;IAED,KAAK;QACH,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,CAAC;QAC1C,IAAI,CAAC,aAAa,GAAG;YACnB,IAAI,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC,MAAM,EAAE,EAAE;gBAC1C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;gBACrB,IAAI,CAAC,eAAe,EAAE,CAAC;gBACvB,IAAI,MAAM,EAAE,CAAC;oBACX,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC;gBACtB,CAAC;YACH,CAAC,CAAC;YACF,IAAI,CAAC,WAAW,CAAC,eAAe,CAAC,GAAG,EAAE;gBACpC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;oBAChB,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC;gBACtB,CAAC;YACH,CAAC,CAAC;SACH,CAAC;QACF,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC;QACtB,CAAC;IACH,CAAC;IAED,IAAI;QACF,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;QACrB,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC;QAC5B,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,EAAE;YACzC,WAAW,EAAE,CAAC;QAChB,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,aAAa,GAAG,EAAE,CAAC;IAC1B,CAAC;IAED,6CAA6C;IAC7C,OAAO;QACL,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,OAAO,IAAI,CAAC,QAAQ,CAAC;QACvB,CAAC;QACD,MAAM,IAAI,GAAkB,OAAO,CAAC,OAAO,EAAE;aAC1C,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;aAClC,OAAO,CAAC,GAAG,EAAE;YACZ,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;gBAC3B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACvB,CAAC;QACH,CAAC,CAAC,CAAC;QACL,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,gEAAgE;IAChE,eAAe;QACb,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;YAC3B,OAAO,IAAI,CAAC,QAAQ,CAAC;QACvB,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;IACxB,CAAC;IAEO,KAAK,CAAC,eAAe;QAC3B,IAAI,KAAK,GAAG,IAAI,CAAC;QACjB,OAAO,KAAK,EAAE,CAAC;YACb,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC;YAC5B,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC;YACnB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACtB,CAAC;YACD,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC;QAC9B,CAAC;IACH,CAAC;IAEO,eAAe;QACrB,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAClC,OAAO;QACT,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,GAAG,EAAE;YAChD,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC;QACtB,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IACtB,CAAC;IAEO,YAAY;QAClB,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;YAC3B,OAAO;QACT,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC9C,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;IACvB,CAAC;CACF"}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Sync activity, as named by the offline-readiness contract. */
|
|
2
|
+
export type SyncStatus = 'idle' | 'syncing' | 'pending' | 'attention' | 'auth' | 'error';
|
|
3
|
+
/** Progress of the initial pull that a cold start must complete. */
|
|
4
|
+
export type InitialPullStatus = 'uninitialized' | 'pulling' | 'settled';
|
|
5
|
+
/** Readiness of the local store, derived from pull progress and delivery. */
|
|
6
|
+
export type LocalStoreReadiness = 'uninitialized' | 'pulling' | 'hydrated-empty' | 'hydrated-populated';
|
|
7
|
+
/** Initial readiness as the offline-readiness contract names it. */
|
|
8
|
+
export type InitialSyncState = 'unknown' | 'syncing' | 'offline-cached' | 'settled';
|
|
9
|
+
/**
|
|
10
|
+
* One rejection that needs product or user action. `entityType` and `entityId`
|
|
11
|
+
* are opaque to baukit; products name their own entities.
|
|
12
|
+
*/
|
|
13
|
+
export type SyncAttentionItem<T = {
|
|
14
|
+
entityType: string;
|
|
15
|
+
entityId: string;
|
|
16
|
+
}> = T;
|
|
17
|
+
export interface SyncStatusSnapshot<TAttention = SyncAttentionItem> {
|
|
18
|
+
status: SyncStatus;
|
|
19
|
+
lastSyncAt: string | null;
|
|
20
|
+
error: string | null;
|
|
21
|
+
attention: readonly TAttention[];
|
|
22
|
+
pendingCount: number;
|
|
23
|
+
initialPullStatus: InitialPullStatus;
|
|
24
|
+
/** Increments whenever a run delivers a new authoritative snapshot. */
|
|
25
|
+
refreshRevision: number;
|
|
26
|
+
/** Set when identity or corruption makes the local store unusable. */
|
|
27
|
+
securityBlock: string | null;
|
|
28
|
+
}
|
|
29
|
+
export interface LocalStoreReadinessInput {
|
|
30
|
+
initialPullStatus: InitialPullStatus;
|
|
31
|
+
refreshRevision: number;
|
|
32
|
+
deliveredRevision: number | null;
|
|
33
|
+
hasData: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface SnakeCaseSyncStatusSnapshot<TAttention = SyncAttentionItem> {
|
|
36
|
+
status: SyncStatus;
|
|
37
|
+
last_sync_at: string | null;
|
|
38
|
+
error: string | null;
|
|
39
|
+
attention: readonly TAttention[];
|
|
40
|
+
pending_count: number;
|
|
41
|
+
initial_pull_status: InitialPullStatus;
|
|
42
|
+
refresh_revision: number;
|
|
43
|
+
security_block: string | null;
|
|
44
|
+
}
|
|
45
|
+
/** Projects a status snapshot onto API-style snake_case field names. */
|
|
46
|
+
export declare function toSnakeCaseSnapshot<TAttention>(snapshot: SyncStatusSnapshot<TAttention>): SnakeCaseSyncStatusSnapshot<TAttention>;
|
|
47
|
+
/**
|
|
48
|
+
* Maps pull progress and snapshot delivery onto local-store readiness.
|
|
49
|
+
*
|
|
50
|
+
* A consumer that has not yet received the newest `refreshRevision` still reads
|
|
51
|
+
* stale data, so it stays `pulling` rather than claiming a settled empty state.
|
|
52
|
+
*/
|
|
53
|
+
export declare function deriveLocalStoreReadiness({ initialPullStatus, refreshRevision, deliveredRevision, hasData, }: LocalStoreReadinessInput): LocalStoreReadiness;
|
|
54
|
+
/** Maps a snapshot onto the contract's initial-readiness vocabulary. */
|
|
55
|
+
export declare function deriveInitialSyncState({ initialPullStatus, lastSyncAt, status, }: Pick<SyncStatusSnapshot, 'initialPullStatus' | 'lastSyncAt' | 'status'>): InitialSyncState;
|
|
56
|
+
/**
|
|
57
|
+
* Observable sync status, with no React and no state library.
|
|
58
|
+
*
|
|
59
|
+
* Products subscribe directly, or adapt {@link SyncStatusStore.subscribe} and
|
|
60
|
+
* {@link SyncStatusStore.getSnapshot} to whatever their UI layer expects.
|
|
61
|
+
*/
|
|
62
|
+
export declare class SyncStatusStore<TAttention = SyncAttentionItem> {
|
|
63
|
+
private snapshot;
|
|
64
|
+
private readonly listeners;
|
|
65
|
+
getSnapshot(): SyncStatusSnapshot<TAttention>;
|
|
66
|
+
subscribe(listener: (snapshot: SyncStatusSnapshot<TAttention>) => void): () => void;
|
|
67
|
+
setSyncing(): void;
|
|
68
|
+
setIdle(lastSyncAt: string): void;
|
|
69
|
+
setAttention(items: readonly TAttention[], pendingCount: number): void;
|
|
70
|
+
setAuth(message: string): void;
|
|
71
|
+
setError(message: string, pendingCount?: number): void;
|
|
72
|
+
/** Restores persisted status on a cold start, before the first run. */
|
|
73
|
+
hydrate(lastSyncAt: string | null, items?: readonly TAttention[], pendingCount?: number): void;
|
|
74
|
+
reset(): void;
|
|
75
|
+
setSecurityBlock(message: string): void;
|
|
76
|
+
resumeAfterAuth(): void;
|
|
77
|
+
private set;
|
|
78
|
+
}
|
|
79
|
+
/** Write-only view a sync run uses to report progress. */
|
|
80
|
+
export interface SyncStatusSink<TAttention = SyncAttentionItem> {
|
|
81
|
+
setSyncing(): void;
|
|
82
|
+
setIdle(lastSyncAt: string): void;
|
|
83
|
+
setAttention(items: readonly TAttention[], pendingCount: number): void;
|
|
84
|
+
setAuth(message: string): void;
|
|
85
|
+
setError(message: string, pendingCount?: number): void;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
function initialSnapshot() {
|
|
2
|
+
return {
|
|
3
|
+
status: 'idle',
|
|
4
|
+
lastSyncAt: null,
|
|
5
|
+
error: null,
|
|
6
|
+
attention: [],
|
|
7
|
+
pendingCount: 0,
|
|
8
|
+
initialPullStatus: 'uninitialized',
|
|
9
|
+
refreshRevision: 0,
|
|
10
|
+
securityBlock: null,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** Projects a status snapshot onto API-style snake_case field names. */
|
|
14
|
+
export function toSnakeCaseSnapshot(snapshot) {
|
|
15
|
+
return {
|
|
16
|
+
status: snapshot.status,
|
|
17
|
+
last_sync_at: snapshot.lastSyncAt,
|
|
18
|
+
error: snapshot.error,
|
|
19
|
+
attention: snapshot.attention,
|
|
20
|
+
pending_count: snapshot.pendingCount,
|
|
21
|
+
initial_pull_status: snapshot.initialPullStatus,
|
|
22
|
+
refresh_revision: snapshot.refreshRevision,
|
|
23
|
+
security_block: snapshot.securityBlock,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Maps pull progress and snapshot delivery onto local-store readiness.
|
|
28
|
+
*
|
|
29
|
+
* A consumer that has not yet received the newest `refreshRevision` still reads
|
|
30
|
+
* stale data, so it stays `pulling` rather than claiming a settled empty state.
|
|
31
|
+
*/
|
|
32
|
+
export function deriveLocalStoreReadiness({ initialPullStatus, refreshRevision, deliveredRevision, hasData, }) {
|
|
33
|
+
if (initialPullStatus === 'uninitialized')
|
|
34
|
+
return 'uninitialized';
|
|
35
|
+
if (initialPullStatus === 'pulling' || deliveredRevision !== refreshRevision)
|
|
36
|
+
return 'pulling';
|
|
37
|
+
return hasData ? 'hydrated-populated' : 'hydrated-empty';
|
|
38
|
+
}
|
|
39
|
+
/** Maps a snapshot onto the contract's initial-readiness vocabulary. */
|
|
40
|
+
export function deriveInitialSyncState({ initialPullStatus, lastSyncAt, status, }) {
|
|
41
|
+
if (initialPullStatus === 'uninitialized')
|
|
42
|
+
return 'unknown';
|
|
43
|
+
if (initialPullStatus === 'pulling')
|
|
44
|
+
return 'syncing';
|
|
45
|
+
if (status === 'error' && lastSyncAt === null)
|
|
46
|
+
return 'offline-cached';
|
|
47
|
+
return 'settled';
|
|
48
|
+
}
|
|
49
|
+
function settledStatus(attentionCount, pendingCount) {
|
|
50
|
+
if (attentionCount > 0)
|
|
51
|
+
return 'attention';
|
|
52
|
+
return pendingCount > 0 ? 'pending' : 'idle';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Observable sync status, with no React and no state library.
|
|
56
|
+
*
|
|
57
|
+
* Products subscribe directly, or adapt {@link SyncStatusStore.subscribe} and
|
|
58
|
+
* {@link SyncStatusStore.getSnapshot} to whatever their UI layer expects.
|
|
59
|
+
*/
|
|
60
|
+
export class SyncStatusStore {
|
|
61
|
+
snapshot = initialSnapshot();
|
|
62
|
+
listeners = new Set();
|
|
63
|
+
getSnapshot() {
|
|
64
|
+
return this.snapshot;
|
|
65
|
+
}
|
|
66
|
+
subscribe(listener) {
|
|
67
|
+
this.listeners.add(listener);
|
|
68
|
+
return () => {
|
|
69
|
+
this.listeners.delete(listener);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
setSyncing() {
|
|
73
|
+
this.set((state) => ({
|
|
74
|
+
status: 'syncing',
|
|
75
|
+
error: null,
|
|
76
|
+
initialPullStatus: state.lastSyncAt === null ? 'pulling' : 'settled',
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
setIdle(lastSyncAt) {
|
|
80
|
+
this.set((state) => ({
|
|
81
|
+
status: 'idle',
|
|
82
|
+
lastSyncAt,
|
|
83
|
+
error: null,
|
|
84
|
+
attention: [],
|
|
85
|
+
pendingCount: 0,
|
|
86
|
+
initialPullStatus: 'settled',
|
|
87
|
+
refreshRevision: state.refreshRevision + 1,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
setAttention(items, pendingCount) {
|
|
91
|
+
this.set((state) => ({
|
|
92
|
+
status: items.length > 0 ? 'attention' : 'pending',
|
|
93
|
+
error: null,
|
|
94
|
+
attention: [...items],
|
|
95
|
+
pendingCount,
|
|
96
|
+
initialPullStatus: 'settled',
|
|
97
|
+
refreshRevision: state.refreshRevision + 1,
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
setAuth(message) {
|
|
101
|
+
this.set((state) => ({
|
|
102
|
+
status: 'auth',
|
|
103
|
+
error: message,
|
|
104
|
+
initialPullStatus: 'settled',
|
|
105
|
+
refreshRevision: state.refreshRevision + 1,
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
setError(message, pendingCount) {
|
|
109
|
+
this.set((state) => {
|
|
110
|
+
const nextPendingCount = pendingCount ?? state.pendingCount;
|
|
111
|
+
return {
|
|
112
|
+
status: state.attention.length > 0 ? 'attention' : nextPendingCount > 0 ? 'pending' : 'error',
|
|
113
|
+
error: message,
|
|
114
|
+
pendingCount: nextPendingCount,
|
|
115
|
+
initialPullStatus: 'settled',
|
|
116
|
+
refreshRevision: state.refreshRevision + 1,
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/** Restores persisted status on a cold start, before the first run. */
|
|
121
|
+
hydrate(lastSyncAt, items = [], pendingCount = 0) {
|
|
122
|
+
this.set((state) => ({
|
|
123
|
+
status: settledStatus(items.length, pendingCount),
|
|
124
|
+
lastSyncAt,
|
|
125
|
+
error: null,
|
|
126
|
+
attention: [...items],
|
|
127
|
+
pendingCount,
|
|
128
|
+
initialPullStatus: lastSyncAt === null ? 'uninitialized' : 'settled',
|
|
129
|
+
refreshRevision: state.refreshRevision + 1,
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
reset() {
|
|
133
|
+
this.set(() => initialSnapshot());
|
|
134
|
+
}
|
|
135
|
+
setSecurityBlock(message) {
|
|
136
|
+
this.set(() => ({
|
|
137
|
+
status: 'error',
|
|
138
|
+
error: message,
|
|
139
|
+
initialPullStatus: 'settled',
|
|
140
|
+
securityBlock: message,
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
resumeAfterAuth() {
|
|
144
|
+
this.set((state) => ({
|
|
145
|
+
status: settledStatus(state.attention.length, state.pendingCount),
|
|
146
|
+
error: null,
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
set(update) {
|
|
150
|
+
this.snapshot = { ...this.snapshot, ...update(this.snapshot) };
|
|
151
|
+
this.listeners.forEach((listener) => {
|
|
152
|
+
listener(this.snapshot);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Minimal `fetch` shape the transport depends on. */
|
|
2
|
+
export interface SyncRequestInit {
|
|
3
|
+
method?: string;
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
body?: string;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
}
|
|
8
|
+
export type SyncFetch = (input: string, init?: SyncRequestInit) => Promise<SyncFetchResponse>;
|
|
9
|
+
/** A product API client's decoded request function. */
|
|
10
|
+
export type SyncPrebuiltRequest = <T>(input: string, init?: SyncRequestInit) => Promise<T>;
|
|
11
|
+
/** Minimal `Response` shape the transport depends on. */
|
|
12
|
+
export interface SyncFetchResponse {
|
|
13
|
+
readonly ok: boolean;
|
|
14
|
+
readonly status: number;
|
|
15
|
+
text(): Promise<string>;
|
|
16
|
+
}
|
|
17
|
+
export interface SyncFetchTransportOptions {
|
|
18
|
+
/** Absolute base URL of the sync API, without a trailing slash. */
|
|
19
|
+
baseUrl: string;
|
|
20
|
+
fetch: SyncFetch;
|
|
21
|
+
/** Resolves the headers that authenticate a request, per attempt. */
|
|
22
|
+
authHeaders: () => Promise<Record<string, string>> | Record<string, string>;
|
|
23
|
+
/**
|
|
24
|
+
* Header carrying the local-data partition the caller expects to talk to.
|
|
25
|
+
* Defaults to `X-Partition-Id`.
|
|
26
|
+
*/
|
|
27
|
+
partitionHeader?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Error code the server returns when the caller's partition no longer
|
|
30
|
+
* exists. Defaults to `partition_identity_mismatch`.
|
|
31
|
+
*/
|
|
32
|
+
partitionMismatchCode?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface SyncPrebuiltRequestTransportOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Sends and decodes a request through a product API client. The function owns
|
|
37
|
+
* base-URL resolution, authentication recovery, and failure classification.
|
|
38
|
+
*/
|
|
39
|
+
request: SyncPrebuiltRequest;
|
|
40
|
+
/** Defaults to `X-Partition-Id`. */
|
|
41
|
+
partitionHeader?: string;
|
|
42
|
+
}
|
|
43
|
+
export type SyncTransportOptions = SyncFetchTransportOptions | SyncPrebuiltRequestTransportOptions;
|
|
44
|
+
export interface SyncRequestOptions {
|
|
45
|
+
method?: string;
|
|
46
|
+
query?: Record<string, string>;
|
|
47
|
+
body?: unknown;
|
|
48
|
+
/** Partition the caller expects; sent as the partition header when present. */
|
|
49
|
+
partitionId?: string | null;
|
|
50
|
+
signal?: AbortSignal;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Sync request plumbing for either a raw fetch or a product API client.
|
|
54
|
+
*
|
|
55
|
+
* Endpoint paths, request bodies, and response shapes stay product-defined:
|
|
56
|
+
* callers name the path and the response type on every call.
|
|
57
|
+
*/
|
|
58
|
+
export declare class SyncTransport {
|
|
59
|
+
private readonly options;
|
|
60
|
+
private readonly baseUrl;
|
|
61
|
+
private readonly partitionHeader;
|
|
62
|
+
private readonly partitionMismatchCode;
|
|
63
|
+
constructor(options: SyncTransportOptions);
|
|
64
|
+
/**
|
|
65
|
+
* Sends one sync request and decodes its JSON response.
|
|
66
|
+
*
|
|
67
|
+
* The fetch-backed variant decodes JSON and maps failures to
|
|
68
|
+
* {@link SyncTransportError}. The prebuilt-request variant delegates response
|
|
69
|
+
* decoding and failure policy to that request function.
|
|
70
|
+
*/
|
|
71
|
+
request<T>(path: string, options?: SyncRequestOptions): Promise<T>;
|
|
72
|
+
private send;
|
|
73
|
+
private init;
|
|
74
|
+
private readBody;
|
|
75
|
+
private decode;
|
|
76
|
+
private url;
|
|
77
|
+
private path;
|
|
78
|
+
private asTransportError;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAEA,sDAAsD;AACtD,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAE9F,uDAAuD;AACvD,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;AAE3F,yDAAyD;AACzD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,yBAAyB;IACxC,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,SAAS,CAAC;IACjB,qEAAqE;IACrE,WAAW,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5E;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,mCAAmC;IAClD;;;OAGG;IACH,OAAO,EAAE,mBAAmB,CAAC;IAC7B,oCAAoC;IACpC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,oBAAoB,GAAG,yBAAyB,GAAG,mCAAmC,CAAC;AAEnG,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAwBD;;;;;GAKG;AACH,qBAAa,aAAa;IAKZ,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAgB;gBAEzB,OAAO,EAAE,oBAAoB;IAS1D;;;;;;OAMG;IACG,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,CAAC,CAAC;YAY9D,IAAI;IAalB,OAAO,CAAC,IAAI;YAwBE,QAAQ;IAQtB,OAAO,CAAC,MAAM;IAWd,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,IAAI;IAKZ,OAAO,CAAC,gBAAgB;CAczB"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { SyncAuthError, SyncPartitionMismatchError, SyncTransportError } from './error.js';
|
|
2
|
+
const DEFAULT_PARTITION_HEADER = 'X-Partition-Id';
|
|
3
|
+
const DEFAULT_PARTITION_MISMATCH_CODE = 'partition_identity_mismatch';
|
|
4
|
+
const RETRYABLE_STATUSES = new Set([408, 429]);
|
|
5
|
+
function isRetryableStatus(status) {
|
|
6
|
+
return RETRYABLE_STATUSES.has(status) || status >= 500;
|
|
7
|
+
}
|
|
8
|
+
function parseErrorBody(body) {
|
|
9
|
+
try {
|
|
10
|
+
const decoded = JSON.parse(body);
|
|
11
|
+
return typeof decoded === 'object' && decoded !== null ? decoded : {};
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sync request plumbing for either a raw fetch or a product API client.
|
|
19
|
+
*
|
|
20
|
+
* Endpoint paths, request bodies, and response shapes stay product-defined:
|
|
21
|
+
* callers name the path and the response type on every call.
|
|
22
|
+
*/
|
|
23
|
+
export class SyncTransport {
|
|
24
|
+
options;
|
|
25
|
+
baseUrl;
|
|
26
|
+
partitionHeader;
|
|
27
|
+
partitionMismatchCode;
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.options = options;
|
|
30
|
+
this.baseUrl = 'fetch' in options ? options.baseUrl.replace(/\/+$/, '') : null;
|
|
31
|
+
this.partitionHeader = options.partitionHeader ?? DEFAULT_PARTITION_HEADER;
|
|
32
|
+
this.partitionMismatchCode =
|
|
33
|
+
'fetch' in options
|
|
34
|
+
? (options.partitionMismatchCode ?? DEFAULT_PARTITION_MISMATCH_CODE)
|
|
35
|
+
: null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Sends one sync request and decodes its JSON response.
|
|
39
|
+
*
|
|
40
|
+
* The fetch-backed variant decodes JSON and maps failures to
|
|
41
|
+
* {@link SyncTransportError}. The prebuilt-request variant delegates response
|
|
42
|
+
* decoding and failure policy to that request function.
|
|
43
|
+
*/
|
|
44
|
+
async request(path, options = {}) {
|
|
45
|
+
if ('request' in this.options) {
|
|
46
|
+
return this.options.request(this.path(path, options.query), this.init(options, {}));
|
|
47
|
+
}
|
|
48
|
+
const response = await this.send(path, options);
|
|
49
|
+
const body = await this.readBody(response);
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw this.asTransportError(response.status, body);
|
|
52
|
+
}
|
|
53
|
+
return this.decode(body);
|
|
54
|
+
}
|
|
55
|
+
async send(path, options) {
|
|
56
|
+
if (!('fetch' in this.options)) {
|
|
57
|
+
throw new Error('unreachable request-backed transport');
|
|
58
|
+
}
|
|
59
|
+
const headers = { ...(await this.options.authHeaders()) };
|
|
60
|
+
const init = this.init(options, headers);
|
|
61
|
+
try {
|
|
62
|
+
return await this.options.fetch(this.url(path, options.query), init);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw new SyncTransportError(errorMessage(error), true, error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
init(options, initialHeaders) {
|
|
69
|
+
const headers = { ...initialHeaders };
|
|
70
|
+
if (options.body !== undefined) {
|
|
71
|
+
headers['Content-Type'] = 'application/json';
|
|
72
|
+
}
|
|
73
|
+
if (options.partitionId != null) {
|
|
74
|
+
headers[this.partitionHeader] = options.partitionId;
|
|
75
|
+
}
|
|
76
|
+
const init = { headers };
|
|
77
|
+
if (options.method !== undefined) {
|
|
78
|
+
init.method = options.method;
|
|
79
|
+
}
|
|
80
|
+
if (options.body !== undefined) {
|
|
81
|
+
init.body = JSON.stringify(options.body);
|
|
82
|
+
}
|
|
83
|
+
if (options.signal !== undefined) {
|
|
84
|
+
init.signal = options.signal;
|
|
85
|
+
}
|
|
86
|
+
return init;
|
|
87
|
+
}
|
|
88
|
+
async readBody(response) {
|
|
89
|
+
try {
|
|
90
|
+
return await response.text();
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
throw new SyncTransportError(errorMessage(error), true, error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
decode(body) {
|
|
97
|
+
if (body.length === 0) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(body);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
throw new SyncTransportError('sync response was not valid JSON', false, error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
url(path, query) {
|
|
108
|
+
return `${this.baseUrl ?? ''}${this.path(path, query)}`;
|
|
109
|
+
}
|
|
110
|
+
path(path, query) {
|
|
111
|
+
const suffix = query ? `?${new URLSearchParams(query).toString()}` : '';
|
|
112
|
+
return `${path}${suffix}`;
|
|
113
|
+
}
|
|
114
|
+
asTransportError(status, body) {
|
|
115
|
+
const decoded = parseErrorBody(body);
|
|
116
|
+
const message = typeof decoded.message === 'string' && decoded.message.length > 0
|
|
117
|
+
? decoded.message
|
|
118
|
+
: `sync request failed with status ${String(status)}`;
|
|
119
|
+
if (this.partitionMismatchCode !== null && decoded.code === this.partitionMismatchCode) {
|
|
120
|
+
return new SyncPartitionMismatchError(message);
|
|
121
|
+
}
|
|
122
|
+
if (status === 401) {
|
|
123
|
+
return new SyncAuthError(message);
|
|
124
|
+
}
|
|
125
|
+
return new SyncTransportError(message, isRetryableStatus(status));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function errorMessage(error) {
|
|
129
|
+
return error instanceof Error ? error.message : String(error);
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@baukit/sync-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Client-side scheduling, transport, status reporting, and ordering for offline-capable sync loops.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Patrick Koss",
|
|
7
|
+
"homepage": "https://github.com/PatrickKoss/baukit#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/PatrickKoss/baukit.git",
|
|
11
|
+
"directory": "typescript/packages/sync-client"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/PatrickKoss/baukit/issues"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=24.0.0"
|
|
22
|
+
},
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./expo": {
|
|
30
|
+
"types": "./dist/expo.d.ts",
|
|
31
|
+
"import": "./dist/expo.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"expo-network": "57.0.1",
|
|
41
|
+
"react-native": "0.86.2",
|
|
42
|
+
"typescript": "6.0.3",
|
|
43
|
+
"vitest": "4.1.10"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"expo-network": "^57.0.1",
|
|
47
|
+
"react-native": "^0.86.2"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"expo-network": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"react-native": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsc -p tsconfig.json",
|
|
59
|
+
"test": "tsc -p tsconfig.exports.json && vitest run --config ../../vitest.config.ts",
|
|
60
|
+
"lint": "eslint src --max-warnings=0",
|
|
61
|
+
"format:check": "prettier --check src type-tests package.json tsconfig*.json"
|
|
62
|
+
}
|
|
63
|
+
}
|