@happyvertical/smrt-web 0.38.8 → 0.38.10
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/AGENTS.md +16 -12
- package/dist/index.d.ts +20 -2
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -223,7 +223,9 @@ external trigger, `teardown` unregisters); it never touches the engine —
|
|
|
223
223
|
`createSmrtWebClient`: construct once, pass to every collection; NOT
|
|
224
224
|
auto-derived per collection — one EventSource / one poll loop feeds all).
|
|
225
225
|
Config: `{ eventsUrl, changesUrl, fetchFn?, eventSourceFactory?,
|
|
226
|
-
pollIntervalMs?=5000, withCredentials?=true }`.
|
|
226
|
+
pollIntervalMs?=5000, withCredentials?=true, manifestHash?, updateState? }`.
|
|
227
|
+
`manifestHash` + `updateState` enable live contract detection from the
|
|
228
|
+
server's connection-open `manifest` SSE frame (#1859). Public surface:
|
|
227
229
|
`{ transport: 'sse'|'polling'|'idle', registerTable(table, invalidate) →
|
|
228
230
|
unregister, invalidateAll(), close() }`.
|
|
229
231
|
- `liveInvalidation({ subscriber, tableName })` — the thin per-collection
|
|
@@ -238,15 +240,17 @@ half in `packages/core/src/generators/`):
|
|
|
238
240
|
- Push — the `_events` SSE route (`events-route.ts`): **NAMED** events
|
|
239
241
|
`event: change` / `event: resync`, `data` is `{table, operation, rowId,
|
|
240
242
|
tenantId}`, and the cursor `seq` is carried **only** in the SSE `id:` field
|
|
241
|
-
(the browser mirrors it to `MessageEvent.lastEventId`). `
|
|
242
|
-
`
|
|
243
|
-
|
|
243
|
+
(the browser mirrors it to `MessageEvent.lastEventId`). `event: manifest`
|
|
244
|
+
carries `{ manifestHash }` at connection open so a reconnect can latch the
|
|
245
|
+
contract update signal. `resync` uses its `id:` as the fresh horizon for any
|
|
246
|
+
later polling downgrade. Heartbeats are `: heartbeat` comment lines
|
|
247
|
+
EventSource ignores natively.
|
|
244
248
|
- Pull — the `_changes` route (`changes-route.ts`): `GET {changesUrl}?since=
|
|
245
249
|
&tables=` → `{changes, cursor, resyncRequired?, resyncCursor?}`, the full
|
|
246
250
|
fallback.
|
|
247
251
|
|
|
248
252
|
**The NAMED-event gotcha.** The frames are named, so the subscriber wires
|
|
249
|
-
`es.addEventListener('change', …)` / `('resync', …)` — **`onmessage` never
|
|
253
|
+
`es.addEventListener('change', …)` / `('resync', …)` / `('manifest', …)` — **`onmessage` never
|
|
250
254
|
fires** for a named event and would silently receive nothing. A `change` frame's
|
|
251
255
|
`data` is JSON-parsed **defensively**: malformed input is logged + dropped, never
|
|
252
256
|
thrown back into the EventSource message loop (a throw there breaks later
|
|
@@ -348,7 +352,8 @@ A tiny pub/sub (`update-state.ts`) with TWO INDEPENDENT signals; `updateAvailabl
|
|
|
348
352
|
`manifestHash` (passed in by the consumer, imported from
|
|
349
353
|
`@happyvertical/smrt-virt-web`) against a persisted "last-seen manifestHash" in
|
|
350
354
|
durable storage; if they differ → fire `contract` + store the new value. First
|
|
351
|
-
run (no baseline) records without firing.
|
|
355
|
+
run (no baseline) records without firing. The live `_events` manifest frame
|
|
356
|
+
can also push the same sticky signal via `notifyContractUpdated()` (#1859).
|
|
352
357
|
|
|
353
358
|
The last-seen hash lives in `update-state/meta-store.ts` (a tiny IDB key/value
|
|
354
359
|
store) under the durable namespace, registered as a durable resource so
|
|
@@ -356,12 +361,11 @@ store) under the durable namespace, registered as a durable resource so
|
|
|
356
361
|
AC). Degrades gracefully if IndexedDB is absent (bundle-only) or no running hash
|
|
357
362
|
is supplied.
|
|
358
363
|
|
|
359
|
-
> **Trade-off
|
|
360
|
-
>
|
|
361
|
-
>
|
|
362
|
-
>
|
|
363
|
-
>
|
|
364
|
-
> is shaped for it.
|
|
364
|
+
> **Trade-off:** live contract detection is reconnect-based. The server hash is
|
|
365
|
+
> advertised when the `_events` stream opens, so a deploy surfaces when the tab
|
|
366
|
+
> reconnects (instant when the deploy drops old SSE connections, otherwise on
|
|
367
|
+
> the next natural reconnect). Reconnect-independent fan-out remains a later
|
|
368
|
+
> enhancement.
|
|
365
369
|
|
|
366
370
|
The reactive Svelte binding (`useUpdateAvailable`) ships in
|
|
367
371
|
`@happyvertical/smrt-svelte/web` — it wires SvelteKit's `updated` store into the
|
package/dist/index.d.ts
CHANGED
|
@@ -133,8 +133,9 @@ export declare function createSmrtWebEventSubscriber(config: SmrtWebEventSubscri
|
|
|
133
133
|
/**
|
|
134
134
|
* Create the framework-free `updateAvailable` primitive. Kicks off async
|
|
135
135
|
* contract detection immediately (compare running vs. persisted manifest hash);
|
|
136
|
-
* {@link UpdateState.notifyBundleUpdated} feeds the bundle signal
|
|
137
|
-
*
|
|
136
|
+
* {@link UpdateState.notifyBundleUpdated} feeds the bundle signal, and
|
|
137
|
+
* {@link UpdateState.notifyContractUpdated} feeds the live contract signal. A
|
|
138
|
+
* change to EITHER signal notifies subscribers.
|
|
138
139
|
*/
|
|
139
140
|
export declare function createUpdateState(config: UpdateStateConfig): UpdateState;
|
|
140
141
|
|
|
@@ -807,6 +808,17 @@ export declare interface SmrtWebEventSubscriberConfig {
|
|
|
807
808
|
pollIntervalMs?: number;
|
|
808
809
|
/** `withCredentials` for the EventSource (cookie auth). Default true. */
|
|
809
810
|
withCredentials?: boolean;
|
|
811
|
+
/**
|
|
812
|
+
* The RUNNING build's web-collection shape digest. When paired with
|
|
813
|
+
* `updateState`, a differing server hash from the `_events` manifest frame
|
|
814
|
+
* latches the contract update signal (#1859).
|
|
815
|
+
*/
|
|
816
|
+
manifestHash?: string;
|
|
817
|
+
/**
|
|
818
|
+
* Optional update-state primitive from `createUpdateState()`. Only its
|
|
819
|
+
* contract signal is used here; bundle polling remains the caller's job.
|
|
820
|
+
*/
|
|
821
|
+
updateState?: Pick<UpdateState, 'notifyContractUpdated'>;
|
|
810
822
|
}
|
|
811
823
|
|
|
812
824
|
/**
|
|
@@ -963,6 +975,12 @@ export declare interface UpdateState {
|
|
|
963
975
|
* (a bundle update does not un-happen).
|
|
964
976
|
*/
|
|
965
977
|
notifyBundleUpdated(): void;
|
|
978
|
+
/**
|
|
979
|
+
* Push the API CONTRACT signal — call when a live transport observes a server
|
|
980
|
+
* `manifestHash` that differs from this running build (#1859). Idempotent:
|
|
981
|
+
* once set it stays set (a contract mismatch does not un-happen).
|
|
982
|
+
*/
|
|
983
|
+
notifyContractUpdated(): void;
|
|
966
984
|
/**
|
|
967
985
|
* Resolves once the async contract detection has settled (compared the running
|
|
968
986
|
* hash against the persisted last-seen value and fired the signal if needed).
|
package/dist/index.js
CHANGED
|
@@ -1108,7 +1108,7 @@ function defaultEventSourceFactory(url, init) {
|
|
|
1108
1108
|
return new EventSourceCtor(url, init);
|
|
1109
1109
|
}
|
|
1110
1110
|
function createSmrtWebEventSubscriber(config) {
|
|
1111
|
-
const { eventsUrl, changesUrl, fetchFn = (...args) => globalThis.fetch(...args), eventSourceFactory = defaultEventSourceFactory, pollIntervalMs = 5e3, withCredentials = true } = config;
|
|
1111
|
+
const { eventsUrl, changesUrl, fetchFn = (...args) => globalThis.fetch(...args), eventSourceFactory = defaultEventSourceFactory, pollIntervalMs = 5e3, withCredentials = true, manifestHash, updateState } = config;
|
|
1112
1112
|
const tableInvalidators = /* @__PURE__ */ new Map();
|
|
1113
1113
|
let lastSeq = null;
|
|
1114
1114
|
let transport = "idle";
|
|
@@ -1170,6 +1170,19 @@ function createSmrtWebEventSubscriber(config) {
|
|
|
1170
1170
|
advanceLastSeqFromEventId(ev.lastEventId);
|
|
1171
1171
|
invalidateAll();
|
|
1172
1172
|
};
|
|
1173
|
+
const onManifest = (ev) => {
|
|
1174
|
+
if (closed || manifestHash === void 0 || !updateState) return;
|
|
1175
|
+
try {
|
|
1176
|
+
const serverHash = JSON.parse(ev.data).manifestHash;
|
|
1177
|
+
if (typeof serverHash !== "string" || serverHash.length === 0) {
|
|
1178
|
+
warn("dropping manifest frame with no manifestHash", ev.data);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
if (serverHash !== manifestHash) updateState.notifyContractUpdated();
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
warn("dropping malformed manifest frame", error);
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1173
1186
|
const poll = async () => {
|
|
1174
1187
|
if (closed) return;
|
|
1175
1188
|
const tables = registeredTables();
|
|
@@ -1207,6 +1220,7 @@ function createSmrtWebEventSubscriber(config) {
|
|
|
1207
1220
|
eventSource = source;
|
|
1208
1221
|
source.addEventListener("change", onChange);
|
|
1209
1222
|
source.addEventListener("resync", onResync);
|
|
1223
|
+
source.addEventListener("manifest", onManifest);
|
|
1210
1224
|
source.onerror = () => {
|
|
1211
1225
|
if (closed) return;
|
|
1212
1226
|
if (source.readyState === EVENT_SOURCE_CLOSED) {
|
|
@@ -1392,6 +1406,7 @@ function createUpdateState(config) {
|
|
|
1392
1406
|
};
|
|
1393
1407
|
},
|
|
1394
1408
|
notifyBundleUpdated: setBundle,
|
|
1409
|
+
notifyContractUpdated: setContract,
|
|
1395
1410
|
ready: (async () => {
|
|
1396
1411
|
const runningHash = config.manifestHash;
|
|
1397
1412
|
if (runningHash === void 0) return;
|