@happyvertical/smrt-web 0.51.32 → 0.51.34
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/dist/__typechecks__/webmcp-tool-definition.d.ts +2 -0
- package/dist/__typechecks__/webmcp-tool-definition.d.ts.map +1 -0
- package/dist/ai.d.ts +330 -358
- package/dist/ai.d.ts.map +1 -0
- package/dist/capability-classification.d.ts +89 -0
- package/dist/capability-classification.d.ts.map +1 -0
- package/dist/capability.d.ts +191 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/data-query.d.ts +121 -0
- package/dist/data-query.d.ts.map +1 -0
- package/dist/data-surface-actions.d.ts +118 -0
- package/dist/data-surface-actions.d.ts.map +1 -0
- package/dist/durable-store.d.ts +88 -0
- package/dist/durable-store.d.ts.map +1 -0
- package/dist/index.d.ts +468 -2245
- package/dist/index.d.ts.map +1 -0
- package/dist/intents.d.ts +273 -300
- package/dist/intents.d.ts.map +1 -0
- package/dist/internal.d.ts +15 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/offline/durable-queue.d.ts +171 -0
- package/dist/offline/durable-queue.d.ts.map +1 -0
- package/dist/offline/engine.d.ts +359 -0
- package/dist/offline/engine.d.ts.map +1 -0
- package/dist/offline/leader.d.ts +57 -0
- package/dist/offline/leader.d.ts.map +1 -0
- package/dist/offline/types.d.ts +232 -0
- package/dist/offline/types.d.ts.map +1 -0
- package/dist/offline.d.ts +258 -0
- package/dist/offline.d.ts.map +1 -0
- package/dist/persistence/snapshot-store.d.ts +121 -0
- package/dist/persistence/snapshot-store.d.ts.map +1 -0
- package/dist/persistence.d.ts +157 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/remote-query.d.ts +61 -0
- package/dist/remote-query.d.ts.map +1 -0
- package/dist/sse-client.d.ts +182 -0
- package/dist/sse-client.d.ts.map +1 -0
- package/dist/update-state/meta-store.d.ts +66 -0
- package/dist/update-state/meta-store.d.ts.map +1 -0
- package/dist/update-state.d.ts +115 -0
- package/dist/update-state.d.ts.map +1 -0
- package/dist/webmcp-tool-names.d.ts +106 -112
- package/dist/webmcp-tool-names.d.ts.map +1 -0
- package/dist/webmcp.d.ts +193 -653
- package/dist/webmcp.d.ts.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @happyvertical/smrt-web — the durable persistence capability (#1764).
|
|
3
|
+
*
|
|
4
|
+
* The read-side twin of the offline outbox (#1762): an opted-in collection
|
|
5
|
+
* WARM-STARTS from a durable browser snapshot on load — rendering stale rows
|
|
6
|
+
* instantly — then the engine revalidates in the background via its normal SWR.
|
|
7
|
+
* A reload therefore paints last-known data with zero network round-trip before
|
|
8
|
+
* the fresh fetch lands. This is the read-cache rehydrate the outbox explicitly
|
|
9
|
+
* left to #1764 (see offline.ts "Known gap").
|
|
10
|
+
*
|
|
11
|
+
* This is a {@link SmrtWebCapability} plug-in, so it lives in its OWN module and
|
|
12
|
+
* an app opts a collection in by passing `persistCollection(config)` in that
|
|
13
|
+
* collection's `capabilities` array. A collection WITHOUT it is byte-for-byte
|
|
14
|
+
* the collection of today (the seam's no-op guarantee) — nothing persists
|
|
15
|
+
* implicitly, which is exactly the "persistence is opt-in per model" acceptance
|
|
16
|
+
* criterion. Sensitive models simply omit the capability and never touch disk.
|
|
17
|
+
*
|
|
18
|
+
* ## How it plugs into the seam
|
|
19
|
+
*
|
|
20
|
+
* - `warmStart(ctx)` — reads the persisted snapshot for
|
|
21
|
+
* `(durableStoreNamespace(namespace), collection)` and returns its rows to
|
|
22
|
+
* seed the cache (the factory's rehydrate path; `preload()` awaits the async
|
|
23
|
+
* return so it suppresses the first `list()`). If the namespace's
|
|
24
|
+
* `manifestHash` changed, the key differs → NO snapshot is found → an empty
|
|
25
|
+
* return → a fresh fetch with NO stale-schema hydration. So "a manifest-hash
|
|
26
|
+
* change drops persisted caches" is AUTOMATIC via the namespace including the
|
|
27
|
+
* hash — no explicit invalidation.
|
|
28
|
+
* - `onAttach(ctx)` — subscribes to the collection's changes (via the seam's
|
|
29
|
+
* engine-free `ctx.subscribe`) and persists the current row snapshot
|
|
30
|
+
* (`ctx.snapshot`) DEBOUNCED (trailing) to avoid thrashing IndexedDB on a
|
|
31
|
+
* burst of writes. Also registers the shared store as a durable resource so a
|
|
32
|
+
* logout `wipeDurableStore` clears it.
|
|
33
|
+
* - `teardown(ctx)` — unsubscribes, unregisters the durable resource BEFORE
|
|
34
|
+
* closing IndexedDB (the #1762 discipline: a later `wipeDurableStore` is then a
|
|
35
|
+
* no-op, never a double-clear), and the LAST detach closes the shared db. The
|
|
36
|
+
* durable SNAPSHOT survives for the next load.
|
|
37
|
+
*
|
|
38
|
+
* ## Namespace segregation + logout wipe (shared with the outbox)
|
|
39
|
+
*
|
|
40
|
+
* The durable namespace folds api base / tenant / identity / manifest hash, so
|
|
41
|
+
* switching users on one device lands on a DIFFERENT IndexedDB database — one
|
|
42
|
+
* user can never read another's persisted rows. The SAME namespace is shared
|
|
43
|
+
* with the outbox (#1762), so registering this store under it means a single
|
|
44
|
+
* {@link wipeDurableStore} on logout clears BOTH the persisted collections AND
|
|
45
|
+
* the outbox queue in one call (the "logout wipe clears persisted data AND
|
|
46
|
+
* outbox state" AC).
|
|
47
|
+
*
|
|
48
|
+
* ## Degrade, don't crash
|
|
49
|
+
*
|
|
50
|
+
* If IndexedDB is unavailable (a probe `open()` throws — private mode, sandboxed
|
|
51
|
+
* iframe), the capability `console.warn`s ONCE and behaves as non-persistent:
|
|
52
|
+
* `warmStart` returns nothing (normal fetch), write-back is a no-op. It never
|
|
53
|
+
* throws — matching the outbox's `probeIndexedDb()` posture.
|
|
54
|
+
*
|
|
55
|
+
* Engine-free public surface: no `@tanstack/*` type appears — inside the
|
|
56
|
+
* engine-absorption boundary (#1761), verified by
|
|
57
|
+
* scripts/check-smrt-web-engine-boundary.mjs.
|
|
58
|
+
*/
|
|
59
|
+
import type { SmrtWebCapability } from './capability.js';
|
|
60
|
+
import type { SmrtWebDataSurfaceIdentity } from './data-surface-actions.js';
|
|
61
|
+
import { type DurableStoreKey } from './durable-store.js';
|
|
62
|
+
/** Default trailing-debounce window (ms) for the write-back. */
|
|
63
|
+
export declare const DEFAULT_PERSIST_DEBOUNCE_MS = 250;
|
|
64
|
+
/**
|
|
65
|
+
* Configuration for {@link persistCollection}. Generic in the collection's row
|
|
66
|
+
* type `TData` so the capability matches the collection it plugs into.
|
|
67
|
+
*/
|
|
68
|
+
export interface PersistCollectionConfig<TData extends object = object> {
|
|
69
|
+
/**
|
|
70
|
+
* The REST collection name this snapshot is keyed by — MUST be the SAME
|
|
71
|
+
* `definition.name` passed to {@link createSmrtCollection} (it is the store
|
|
72
|
+
* key under the namespace). Provided explicitly (not read off the ctx) so the
|
|
73
|
+
* config is self-describing and testable.
|
|
74
|
+
*/
|
|
75
|
+
collection: string;
|
|
76
|
+
/**
|
|
77
|
+
* The durable-store identity this snapshot is namespaced under — folds api
|
|
78
|
+
* base / tenant / identity / manifest hash, so a logout, tenant switch, or a
|
|
79
|
+
* contract-changing deploy lands on a different IndexedDB database (never
|
|
80
|
+
* cross-identity reuse, and a shape change drops old snapshots). Shared with
|
|
81
|
+
* the outbox (#1762) via the same {@link durableStoreNamespace}, so one
|
|
82
|
+
* {@link wipeDurableStore} clears both.
|
|
83
|
+
*
|
|
84
|
+
* The canonical `manifestHash` source is the build-time inject — the
|
|
85
|
+
* `manifestHash` constant the generated `@happyvertical/smrt-virt-web` module
|
|
86
|
+
* exports (#1764). Thread it here at the call site.
|
|
87
|
+
*/
|
|
88
|
+
namespace: DurableStoreKey;
|
|
89
|
+
/**
|
|
90
|
+
* Trailing-debounce window (ms) for the write-back snapshot. A burst of
|
|
91
|
+
* mutations within the window collapses to ONE IndexedDB write of the final
|
|
92
|
+
* row set. Default {@link DEFAULT_PERSIST_DEBOUNCE_MS}. Set 0 to write on
|
|
93
|
+
* every change (tests).
|
|
94
|
+
*/
|
|
95
|
+
debounceMs?: number;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Build a durable persistence capability for a collection. Add the returned
|
|
99
|
+
* capability to the collection's `capabilities` array; a collection without it
|
|
100
|
+
* is unaffected (the seam's no-op guarantee — the "opt-in per model" AC).
|
|
101
|
+
*
|
|
102
|
+
* The same `namespace` across multiple collections shares ONE snapshot database
|
|
103
|
+
* (one open, one durable resource, one wipe), and each collection is keyed by
|
|
104
|
+
* its own `collection` name within it.
|
|
105
|
+
*/
|
|
106
|
+
export declare function persistCollection<TData extends object = object>(config: PersistCollectionConfig<TData>): SmrtWebCapability<TData>;
|
|
107
|
+
/** Configuration for {@link persistDataSurface}. */
|
|
108
|
+
export interface PersistDataSurfaceConfig {
|
|
109
|
+
/**
|
|
110
|
+
* The durable-store identity — the SAME key the app's collections and
|
|
111
|
+
* outbox use. It folds the principal (`identityId`) and tenant, so a surface
|
|
112
|
+
* whose rows the server scopes per principal lands on a per-principal
|
|
113
|
+
* database: another user on the device never reads them, and one
|
|
114
|
+
* `wipeDurableStore(namespace)` on logout clears them with everything else.
|
|
115
|
+
*/
|
|
116
|
+
namespace: DurableStoreKey;
|
|
117
|
+
/** The mounted surface's identity (`descriptor.identity`) — the store key. */
|
|
118
|
+
identity: SmrtWebDataSurfaceIdentity;
|
|
119
|
+
/** Trailing-debounce window for `save` (ms). Default {@link DEFAULT_PERSIST_DEBOUNCE_MS}. */
|
|
120
|
+
debounceMs?: number;
|
|
121
|
+
}
|
|
122
|
+
/** A durable row snapshot for one mounted data surface (#3021). */
|
|
123
|
+
export interface PersistedDataSurface<TRow extends object = object> {
|
|
124
|
+
/**
|
|
125
|
+
* The rows last saved for this surface under this namespace, or `undefined`
|
|
126
|
+
* when none exist (first load, a different principal/tenant/manifest, a
|
|
127
|
+
* wipe, or no IndexedDB). Render them as stale, then revalidate. After a
|
|
128
|
+
* `wipeDurableStore` of its namespace a handle is inert (`load` finds
|
|
129
|
+
* nothing, `save` is ignored); create a new one for the next scope.
|
|
130
|
+
*/
|
|
131
|
+
load(): Promise<TRow[] | undefined>;
|
|
132
|
+
/**
|
|
133
|
+
* Replace the persisted rows (debounced, trailing; last call wins). Pass the
|
|
134
|
+
* surface's CURRENT rows whenever the page refreshes them. Rows must be
|
|
135
|
+
* structured-clone-safe plain objects.
|
|
136
|
+
*/
|
|
137
|
+
save(rows: readonly TRow[]): void;
|
|
138
|
+
/** Write any pending `save` now. */
|
|
139
|
+
flush(): Promise<void>;
|
|
140
|
+
/** Flush a pending `save`, then release the shared store. */
|
|
141
|
+
dispose(): Promise<void>;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Persist and rehydrate a mounted data surface's rows (#3021) — the
|
|
145
|
+
* `persistCollection` twin for a surface (e.g. one registered with
|
|
146
|
+
* `mountListDataSurface`) whose rows come from a server-scoped data-surface
|
|
147
|
+
* query rather than a generated `list` route. The page owns its rows, so it
|
|
148
|
+
* owns the calls: `load()` before the first fetch to paint stale rows, then
|
|
149
|
+
* `save(rows)` whenever it refreshes them.
|
|
150
|
+
*
|
|
151
|
+
* Shares the namespace's snapshot database, durable-resource registration,
|
|
152
|
+
* and IndexedDB-unavailable degrade with {@link persistCollection}: a
|
|
153
|
+
* manifest-hash, tenant, or identity change lands on a different database, and
|
|
154
|
+
* `wipeDurableStore(namespace)` clears the surface's rows with the rest.
|
|
155
|
+
*/
|
|
156
|
+
export declare function persistDataSurface<TRow extends object = object>(config: PersistDataSurfaceConfig): PersistedDataSurface<TRow>;
|
|
157
|
+
//# sourceMappingURL=persistence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../src/persistence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAC5E,OAAO,EACL,KAAK,eAAe,EAGrB,MAAM,oBAAoB,CAAC;AAO5B,gEAAgE;AAChE,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C;;;GAGG;AACH,MAAM,WAAW,uBAAuB,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM;IACpE;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;OAWG;IACH,SAAS,EAAE,eAAe,CAAC;IAC3B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAqID;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,EAC7D,MAAM,EAAE,uBAAuB,CAAC,KAAK,CAAC,GACrC,iBAAiB,CAAC,KAAK,CAAC,CAqJ1B;AAED,oDAAoD;AACpD,MAAM,WAAW,wBAAwB;IACvC;;;;;;OAMG;IACH,SAAS,EAAE,eAAe,CAAC;IAC3B,8EAA8E;IAC9E,QAAQ,EAAE,0BAA0B,CAAC;IACrC,6FAA6F;IAC7F,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,mEAAmE;AACnE,MAAM,WAAW,oBAAoB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM;IAChE;;;;;;OAMG;IACH,IAAI,IAAI,OAAO,CAAC,IAAI,EAAE,GAAG,SAAS,CAAC,CAAC;IACpC;;;;OAIG;IACH,IAAI,CAAC,IAAI,EAAE,SAAS,IAAI,EAAE,GAAG,IAAI,CAAC;IAClC,oCAAoC;IACpC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,6DAA6D;IAC7D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAsBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,EAC7D,MAAM,EAAE,wBAAwB,GAC/B,oBAAoB,CAAC,IAAI,CAAC,CAiF5B"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type SmrtWebDataQueryRequest, type SmrtWebDataQueryResult, type SmrtWebDataQueryTransport } from './data-query.js';
|
|
2
|
+
import type { SmrtWebCollection } from './index.js';
|
|
3
|
+
/** How a query run is allowed to affect the visible query state. */
|
|
4
|
+
export type SmrtWebQueryMode = 'visible' | 'background' | 'prefetch' | 'silent';
|
|
5
|
+
export interface SmrtWebQueryRunOptions {
|
|
6
|
+
mode?: SmrtWebQueryMode;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
/** Cancel this run after the given number of milliseconds. */
|
|
9
|
+
deadlineMs?: number;
|
|
10
|
+
/** Bypass the keyed cache and execute a fresh request (used by refresh). */
|
|
11
|
+
force?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface SmrtWebQueryPage {
|
|
14
|
+
kind: 'offset' | 'cursor';
|
|
15
|
+
limit: number;
|
|
16
|
+
offset?: number;
|
|
17
|
+
nextCursor?: string;
|
|
18
|
+
hasMore: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface SmrtWebQueryState<TData extends object = object> {
|
|
21
|
+
readonly rows: ReadonlyArray<TData>;
|
|
22
|
+
readonly page: SmrtWebQueryPage | undefined;
|
|
23
|
+
readonly total: SmrtWebDataQueryResult['total'] | undefined;
|
|
24
|
+
readonly loading: boolean;
|
|
25
|
+
readonly refreshing: boolean;
|
|
26
|
+
readonly stale: boolean;
|
|
27
|
+
readonly error: unknown;
|
|
28
|
+
readonly lastUpdated: number | undefined;
|
|
29
|
+
readonly result: SmrtWebDataQueryResult | undefined;
|
|
30
|
+
}
|
|
31
|
+
export interface SmrtWebQueryLiveSubscription {
|
|
32
|
+
unsubscribe(): void;
|
|
33
|
+
reconnect(): void;
|
|
34
|
+
}
|
|
35
|
+
export interface SmrtWebQueryTransport extends SmrtWebDataQueryTransport {
|
|
36
|
+
/** Subscribe to changes for this exact query, not the entire collection. */
|
|
37
|
+
subscribe?(request: SmrtWebDataQueryRequest, onResult: (result: unknown) => void, options?: {
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
}): SmrtWebQueryLiveSubscription | {
|
|
40
|
+
unsubscribe(): void;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export interface SmrtWebQuery<TData extends object = object> {
|
|
44
|
+
readonly state: SmrtWebQueryState<TData>;
|
|
45
|
+
readonly request: SmrtWebDataQueryRequest | undefined;
|
|
46
|
+
execute(request: SmrtWebDataQueryRequest, options?: SmrtWebQueryRunOptions): Promise<SmrtWebDataQueryResult>;
|
|
47
|
+
refresh(options?: Omit<SmrtWebQueryRunOptions, 'mode' | 'force'>): Promise<SmrtWebDataQueryResult | undefined>;
|
|
48
|
+
retry(): Promise<SmrtWebDataQueryResult | undefined>;
|
|
49
|
+
subscribe(listener: (state: SmrtWebQueryState<TData>) => void): () => void;
|
|
50
|
+
subscribeLive(): SmrtWebQueryLiveSubscription | undefined;
|
|
51
|
+
invalidate(): void;
|
|
52
|
+
dispose(): void;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Create a query controller over one SMRT collection. Query rows stay in a
|
|
56
|
+
* separate keyed cache; the collection's full list is never loaded.
|
|
57
|
+
*/
|
|
58
|
+
export declare function createSmrtWebQuery<TData extends object>(_collection: SmrtWebCollection<TData>, transport: SmrtWebQueryTransport, options?: {
|
|
59
|
+
staleTimeMs?: number;
|
|
60
|
+
}): SmrtWebQuery<TData>;
|
|
61
|
+
//# sourceMappingURL=remote-query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-query.d.ts","sourceRoot":"","sources":["../src/remote-query.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @happyvertical/smrt-web — live-updates subscriber (#1763, CLIENT half).
|
|
3
|
+
*
|
|
4
|
+
* The browser half of live cache invalidation: ONE app-wide subscriber that
|
|
5
|
+
* turns the server's coarse change signals into collection refetches, so a
|
|
6
|
+
* dashboard reflects another session's writes without a manual refresh or
|
|
7
|
+
* aggressive polling. It speaks the two channels the #1763 server half
|
|
8
|
+
* generated:
|
|
9
|
+
*
|
|
10
|
+
* - the push channel — the generated `_events` Server-Sent-Events route
|
|
11
|
+
* (`packages/core/src/generators/events-route.ts`): NAMED events
|
|
12
|
+
* `event: change` / `event: resync` whose `data` is `{table, operation,
|
|
13
|
+
* rowId, tenantId}`, with the cursor `seq` carried ONLY in the SSE `id:`
|
|
14
|
+
* field (mirrored by the browser to `MessageEvent.lastEventId`), plus
|
|
15
|
+
* `event: manifest` for live contract detection. Heartbeats are
|
|
16
|
+
* `: heartbeat` comment lines EventSource ignores natively.
|
|
17
|
+
* - the pull channel — the generated `_changes` route
|
|
18
|
+
* (`packages/core/src/generators/changes-route.ts`): `GET {changesUrl}
|
|
19
|
+
* ?since=&tables=&limit=` →
|
|
20
|
+
* `{changes, cursor, resyncRequired?, resyncCursor?}`, the full fallback
|
|
21
|
+
* where SSE is unavailable.
|
|
22
|
+
*
|
|
23
|
+
* ## No row payload, no tenant logic, no authorization here
|
|
24
|
+
*
|
|
25
|
+
* The wire carries only a signal (table + row id + operation), never a row.
|
|
26
|
+
* A signal makes the subscriber INVALIDATE — the refetch re-reads through the
|
|
27
|
+
* authorized collection routes, so authorization and tenant-scoping stay
|
|
28
|
+
* ENTIRELY on the read path, exactly as the server enforces (the `_events`
|
|
29
|
+
* stream is auth-guarded and tenant-scoped at connection open; `_changes` is
|
|
30
|
+
* tenant-scoped per request). This module therefore does NO tenant filtering
|
|
31
|
+
* and needs no identity — it just maps `table → invalidate`.
|
|
32
|
+
*
|
|
33
|
+
* ## One app-wide instance (mirrors createSmrtWebClient)
|
|
34
|
+
*
|
|
35
|
+
* A consumer constructs ONE subscriber and passes it to every
|
|
36
|
+
* {@link liveInvalidation} capability — the same "one shared handle" convention
|
|
37
|
+
* as {@link createSmrtWebClient}. It is NOT auto-derived per collection: one
|
|
38
|
+
* EventSource / one poll loop feeds every registered collection.
|
|
39
|
+
*
|
|
40
|
+
* ## Idempotent invalidation → no client-side dedup
|
|
41
|
+
*
|
|
42
|
+
* Re-invalidating on a replayed change (a reconnect replays the tail from the
|
|
43
|
+
* cursor) is safe: invalidation only schedules a background refetch, and the
|
|
44
|
+
* factory's relationship-derived invalidation is itself idempotent
|
|
45
|
+
* (over-invalidation merely refetches). So the subscriber deliberately does NO
|
|
46
|
+
* seq dedup — a replayed signal STILL fires, which is exactly what guarantees a
|
|
47
|
+
* gap misses no invalidation. `lastSeq` is a resume cursor for the poll
|
|
48
|
+
* fallback, not a dedup filter.
|
|
49
|
+
*
|
|
50
|
+
* Engine boundary: no `@happyvertical/smrt-*` import and no `@tanstack/*`
|
|
51
|
+
* import — URLs and `fetch` arrive as config, and the refetch primitive arrives
|
|
52
|
+
* as the `invalidate` callback each capability wires from `ctx.invalidate()`.
|
|
53
|
+
* smrt-web has no logger dependency, so faults are surfaced via `console.warn`
|
|
54
|
+
* (matching `warnCapability` / core's `deliverLocally` style).
|
|
55
|
+
*
|
|
56
|
+
* The server may also emit a named `manifest` event at connection open (#1859):
|
|
57
|
+
* `{ manifestHash }`. When this differs from the running build hash configured
|
|
58
|
+
* here, the subscriber latches `updateState.contract`.
|
|
59
|
+
*/
|
|
60
|
+
import type { SmrtWebCapability } from './capability.js';
|
|
61
|
+
import type { UpdateState } from './update-state.js';
|
|
62
|
+
/**
|
|
63
|
+
* The minimal `EventSource` surface the subscriber uses — declared here so a
|
|
64
|
+
* test injects a fake without a real DOM, and so this module compiles without
|
|
65
|
+
* DOM `lib` beyond the ambient global. A real `EventSource` satisfies it
|
|
66
|
+
* structurally.
|
|
67
|
+
*/
|
|
68
|
+
export interface SmrtWebEventSource {
|
|
69
|
+
/** Native reconnect fires this after (re)connect. */
|
|
70
|
+
onopen: ((this: unknown, ev: unknown) => unknown) | null;
|
|
71
|
+
/**
|
|
72
|
+
* Fires on a stream error. A transient drop leaves `readyState` at OPEN
|
|
73
|
+
* (0→…) and the browser auto-reconnects with `Last-Event-ID`; a fatal error
|
|
74
|
+
* (server 401 / route disabled) leaves it CLOSED.
|
|
75
|
+
*/
|
|
76
|
+
onerror: ((this: unknown, ev: unknown) => unknown) | null;
|
|
77
|
+
/**
|
|
78
|
+
* Fires only for UNNAMED (`message`) events. The `_events` frames are NAMED
|
|
79
|
+
* (`change` / `resync`), so this never fires for them — the subscriber uses
|
|
80
|
+
* {@link addEventListener} instead. Present only to satisfy the structural
|
|
81
|
+
* type of a real EventSource.
|
|
82
|
+
*/
|
|
83
|
+
onmessage: ((this: unknown, ev: unknown) => unknown) | null;
|
|
84
|
+
/** Register a listener for a NAMED event (`change`, `resync`). */
|
|
85
|
+
addEventListener(type: string, listener: (ev: {
|
|
86
|
+
data: string;
|
|
87
|
+
lastEventId: string;
|
|
88
|
+
}) => void): void;
|
|
89
|
+
/** Close the stream (stops reconnection). */
|
|
90
|
+
close(): void;
|
|
91
|
+
/** `CONNECTING` (0), `OPEN` (1), or `CLOSED` (2). */
|
|
92
|
+
readonly readyState: number;
|
|
93
|
+
}
|
|
94
|
+
/** Factory that constructs an {@link SmrtWebEventSource} for a URL. */
|
|
95
|
+
export type SmrtWebEventSourceFactory = (url: string, init: {
|
|
96
|
+
withCredentials: boolean;
|
|
97
|
+
}) => SmrtWebEventSource | null | undefined;
|
|
98
|
+
/** Configuration for {@link createSmrtWebEventSubscriber}. */
|
|
99
|
+
export interface SmrtWebEventSubscriberConfig {
|
|
100
|
+
/** Absolute or same-origin URL of the generated `_events` SSE route. */
|
|
101
|
+
eventsUrl: string;
|
|
102
|
+
/** Absolute or same-origin URL of the generated `_changes` route (fallback). */
|
|
103
|
+
changesUrl: string;
|
|
104
|
+
/** `fetch` implementation for the polling fallback. Defaults to global fetch. */
|
|
105
|
+
fetchFn?: typeof fetch;
|
|
106
|
+
/**
|
|
107
|
+
* Factory for the EventSource. Defaults to `new globalThis.EventSource(url,
|
|
108
|
+
* { withCredentials })`, guarded by a `typeof` check — when EventSource is
|
|
109
|
+
* absent (or the factory yields nothing) the subscriber starts on the polling
|
|
110
|
+
* fallback instead.
|
|
111
|
+
*/
|
|
112
|
+
eventSourceFactory?: SmrtWebEventSourceFactory;
|
|
113
|
+
/** Poll interval for the `_changes` fallback (ms). Default 5000. */
|
|
114
|
+
pollIntervalMs?: number;
|
|
115
|
+
/** `withCredentials` for the EventSource (cookie auth). Default true. */
|
|
116
|
+
withCredentials?: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* The RUNNING build's web-collection shape digest. When paired with
|
|
119
|
+
* `updateState`, a differing server hash from the `_events` manifest frame
|
|
120
|
+
* latches the contract update signal (#1859).
|
|
121
|
+
*/
|
|
122
|
+
manifestHash?: string;
|
|
123
|
+
/**
|
|
124
|
+
* Optional update-state primitive from `createUpdateState()`. Only its
|
|
125
|
+
* contract signal is used here; bundle polling remains the caller's job.
|
|
126
|
+
*/
|
|
127
|
+
updateState?: Pick<UpdateState, 'notifyContractUpdated'>;
|
|
128
|
+
}
|
|
129
|
+
/** Which transport a subscriber is currently using. */
|
|
130
|
+
export type SmrtWebSubscriberTransport = 'sse' | 'polling' | 'idle';
|
|
131
|
+
/**
|
|
132
|
+
* The ONE app-wide live-updates subscriber. Construct once (see
|
|
133
|
+
* {@link createSmrtWebEventSubscriber}) and pass it to every
|
|
134
|
+
* {@link liveInvalidation} capability.
|
|
135
|
+
*/
|
|
136
|
+
export interface SmrtWebEventSubscriber {
|
|
137
|
+
/**
|
|
138
|
+
* The live transport: `'sse'` while the EventSource is the source of truth,
|
|
139
|
+
* `'polling'` when running (or downgraded to) the `_changes` fallback,
|
|
140
|
+
* `'idle'` before any transport starts.
|
|
141
|
+
*/
|
|
142
|
+
readonly transport: SmrtWebSubscriberTransport;
|
|
143
|
+
/**
|
|
144
|
+
* Register an invalidator for a physical table. Returns an unregister
|
|
145
|
+
* function. Multiple invalidators may share a table (two live collections);
|
|
146
|
+
* a `change` for that table fires them all.
|
|
147
|
+
*/
|
|
148
|
+
registerTable(table: string, invalidate: () => void): () => void;
|
|
149
|
+
/** Invalidate every registered table (a `resync` / `resyncRequired`). */
|
|
150
|
+
invalidateAll(): void;
|
|
151
|
+
/** Tear down the live transport and drop all registrations. */
|
|
152
|
+
close(): void;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Create the app-wide live-updates subscriber. Feature-detects ONCE at
|
|
156
|
+
* construction: an available EventSource connects the SSE stream; otherwise it
|
|
157
|
+
* starts the `_changes` poll loop. A fatal SSE error later downgrades to
|
|
158
|
+
* polling for the rest of the subscriber's life (no flap-back).
|
|
159
|
+
*/
|
|
160
|
+
export declare function createSmrtWebEventSubscriber(config: SmrtWebEventSubscriberConfig): SmrtWebEventSubscriber;
|
|
161
|
+
/** Configuration for the {@link liveInvalidation} capability. */
|
|
162
|
+
export interface LiveInvalidationConfig {
|
|
163
|
+
/** The one app-wide subscriber from {@link createSmrtWebEventSubscriber}. */
|
|
164
|
+
subscriber: Pick<SmrtWebEventSubscriber, 'registerTable' | 'invalidateAll' | 'transport'>;
|
|
165
|
+
/**
|
|
166
|
+
* The PHYSICAL table name this collection reads. EXPLICIT because a
|
|
167
|
+
* `SmrtWebCollectionDefinition` has no physical-table field and STI children
|
|
168
|
+
* share one base table — the subscriber keys signals by physical table, so a
|
|
169
|
+
* guess would mis-route invalidations.
|
|
170
|
+
*/
|
|
171
|
+
tableName: string;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* A thin per-collection capability that subscribes THIS collection to live
|
|
175
|
+
* signals for its `tableName`. On attach it registers `ctx.invalidate` (the
|
|
176
|
+
* factory's relationship-derived refetch primitive) with the shared subscriber;
|
|
177
|
+
* on teardown it unregisters. All transport, reconnection, and fan-out live in
|
|
178
|
+
* the {@link createSmrtWebEventSubscriber}; this capability is just the wire
|
|
179
|
+
* between one collection and that one subscriber.
|
|
180
|
+
*/
|
|
181
|
+
export declare function liveInvalidation<TData extends object = object>(config: LiveInvalidationConfig): SmrtWebCapability<TData>;
|
|
182
|
+
//# sourceMappingURL=sse-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse-client.d.ts","sourceRoot":"","sources":["../src/sse-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,qDAAqD;IACrD,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,KAAK,OAAO,CAAC,GAAG,IAAI,CAAC;IACzD;;;;OAIG;IACH,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,KAAK,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1D;;;;;OAKG;IACH,SAAS,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,KAAK,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5D,kEAAkE;IAClE,gBAAgB,CACd,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,CAAC,EAAE,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,GAC5D,IAAI,CAAC;IACR,6CAA6C;IAC7C,KAAK,IAAI,IAAI,CAAC;IACd,qDAAqD;IACrD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAKD,uEAAuE;AACvE,MAAM,MAAM,yBAAyB,GAAG,CACtC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,eAAe,EAAE,OAAO,CAAA;CAAE,KAC/B,kBAAkB,GAAG,IAAI,GAAG,SAAS,CAAC;AAE3C,8DAA8D;AAC9D,MAAM,WAAW,4BAA4B;IAC3C,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,UAAU,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,yBAAyB,CAAC;IAC/C,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,WAAW,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,uBAAuB,CAAC,CAAC;CAC1D;AAED,uDAAuD;AACvD,MAAM,MAAM,0BAA0B,GAAG,KAAK,GAAG,SAAS,GAAG,MAAM,CAAC;AAEpE;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,0BAA0B,CAAC;IAC/C;;;;OAIG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IACjE,yEAAyE;IACzE,aAAa,IAAI,IAAI,CAAC;IACtB,+DAA+D;IAC/D,KAAK,IAAI,IAAI,CAAC;CACf;AAmCD;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,4BAA4B,GACnC,sBAAsB,CAkRxB;AAED,iEAAiE;AACjE,MAAM,WAAW,sBAAsB;IACrC,6EAA6E;IAC7E,UAAU,EAAE,IAAI,CACd,sBAAsB,EACtB,eAAe,GAAG,eAAe,GAAG,WAAW,CAChD,CAAC;IACF;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,EAC5D,MAAM,EAAE,sBAAsB,GAC7B,iBAAiB,CAAC,KAAK,CAAC,CAa1B"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @happyvertical/smrt-web — the durable version-meta store (#1764).
|
|
3
|
+
*
|
|
4
|
+
* A tiny raw IndexedDB key/value store for version-awareness bookkeeping —
|
|
5
|
+
* currently the single "last-seen manifestHash" per durable-store namespace,
|
|
6
|
+
* which contract-change detection ({@link ../update-state}) compares the running
|
|
7
|
+
* build against across loads. One database per namespace
|
|
8
|
+
* ({@link durableStoreNamespace}), one object store (`meta`) keyed by a string
|
|
9
|
+
* key, each record `{ key, value }`.
|
|
10
|
+
*
|
|
11
|
+
* Separate from the collection snapshot store (persistence/snapshot-store.ts):
|
|
12
|
+
* that one holds per-collection row blobs; this holds scalar version state, and
|
|
13
|
+
* update-awareness must work even for an app that persists no collection. Both
|
|
14
|
+
* register under the SAME namespace so one {@link wipeDurableStore} clears
|
|
15
|
+
* everything on logout.
|
|
16
|
+
*
|
|
17
|
+
* Raw IndexedDB, ZERO new runtime dependency — inside the engine-absorption
|
|
18
|
+
* boundary (#1761), mirroring offline/durable-queue.ts.
|
|
19
|
+
*/
|
|
20
|
+
/** IndexedDB object-store name inside every version-meta database. */
|
|
21
|
+
export declare const META_STORE = "meta";
|
|
22
|
+
/** IndexedDB schema version. Bump only on a store/index migration. */
|
|
23
|
+
export declare const META_DB_VERSION = 1;
|
|
24
|
+
/**
|
|
25
|
+
* Suffix appended to the durable-store namespace to form this store's OWN
|
|
26
|
+
* IndexedDB database name — DISTINCT from both the merged outbox's bare-namespace
|
|
27
|
+
* DB and the snapshot store's `::snapshots` DB. Required so persistence + outbox
|
|
28
|
+
* + version-meta under one shared namespace never collide on a single-store v1
|
|
29
|
+
* database (a second opener would find the existing v1 DB, skip
|
|
30
|
+
* `onupgradeneeded`, and throw `NotFoundError` on `objectStore('meta')`).
|
|
31
|
+
* `wipeDurableStore` still clears all three via each registered `clear()`.
|
|
32
|
+
*/
|
|
33
|
+
export declare const META_DB_SUFFIX = "::meta";
|
|
34
|
+
/** The reserved key under which the last-seen manifest hash is stored. */
|
|
35
|
+
export declare const LAST_SEEN_MANIFEST_HASH_KEY = "lastSeenManifestHash";
|
|
36
|
+
/**
|
|
37
|
+
* The durable version-meta store over one IndexedDB database. Constructed via
|
|
38
|
+
* {@link openVersionMetaStore}; used through the async methods below.
|
|
39
|
+
*/
|
|
40
|
+
export declare class VersionMetaStore {
|
|
41
|
+
private readonly db;
|
|
42
|
+
/** The IndexedDB database name (== the durable-store namespace). */
|
|
43
|
+
readonly dbName: string;
|
|
44
|
+
constructor(db: IDBDatabase, dbName: string);
|
|
45
|
+
/** Read the value for `key`, or `undefined` if unset / malformed. */
|
|
46
|
+
get(key: string): Promise<string | undefined>;
|
|
47
|
+
/** Write (replacing) the value for `key`; resolves once durably committed. */
|
|
48
|
+
set(key: string, value: string): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Drop EVERY meta record — the durable-store `clear()` for
|
|
51
|
+
* {@link wipeDurableStore}, so a logout also clears the last-seen manifest
|
|
52
|
+
* hash (the AC "wipe clears the last-seen-hash record").
|
|
53
|
+
*/
|
|
54
|
+
clear(): Promise<void>;
|
|
55
|
+
/** Close the underlying database handle. */
|
|
56
|
+
close(): void;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Open (creating/upgrading as needed) the version-meta database for `namespace`
|
|
60
|
+
* and wrap it in a {@link VersionMetaStore}. `namespace` MUST be a
|
|
61
|
+
* {@link durableStoreNamespace} string. The actual IndexedDB database name is
|
|
62
|
+
* `${namespace}${META_DB_SUFFIX}` — a DISTINCT database from the outbox's and
|
|
63
|
+
* the snapshot store's, so the three never collide (see {@link META_DB_SUFFIX}).
|
|
64
|
+
*/
|
|
65
|
+
export declare function openVersionMetaStore(namespace: string): Promise<VersionMetaStore>;
|
|
66
|
+
//# sourceMappingURL=meta-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta-store.d.ts","sourceRoot":"","sources":["../../src/update-state/meta-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,sEAAsE;AACtE,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,sEAAsE;AACtE,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,WAAW,CAAC;AACvC,0EAA0E;AAC1E,eAAO,MAAM,2BAA2B,yBAAyB,CAAC;AA4BlE;;;GAGG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAc;IACjC,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM;IAK3C,qEAAqE;IAC/D,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAWnD,8EAA8E;IACxE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOpD;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5B,4CAA4C;IAC5C,KAAK,IAAI,IAAI;CAGd;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,gBAAgB,CAAC,CA4B3B"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @happyvertical/smrt-web — the framework-free `updateAvailable` primitive (#1764).
|
|
3
|
+
*
|
|
4
|
+
* A tiny pub/sub carrying an app's "an update is available" state, driven by TWO
|
|
5
|
+
* INDEPENDENT signals so the presentation (a toast / reload prompt, shipped as a
|
|
6
|
+
* Svelte binding in smrt-svelte) can react to either:
|
|
7
|
+
*
|
|
8
|
+
* - **bundle** — the client BUNDLE changed on the server (a new deploy of the
|
|
9
|
+
* front-end assets). SvelteKit already detects this via its native `updated`
|
|
10
|
+
* store (version.json polling); the consumer/smrt-svelte binding pushes it in
|
|
11
|
+
* here through {@link UpdateState.notifyBundleUpdated}. This module owns no
|
|
12
|
+
* polling — it just holds the state and fans out.
|
|
13
|
+
* - **contract** — the API CONTRACT changed: the manifest hash, which only SMRT
|
|
14
|
+
* knows. Detected here under the BUILD-TIME-INJECT decision (#1764): on init
|
|
15
|
+
* we compare the RUNNING build's `manifestHash` (passed in by the consumer,
|
|
16
|
+
* imported from `@happyvertical/smrt-virt-web`) against a persisted
|
|
17
|
+
* "last-seen manifestHash" in durable storage. If they differ, the contract
|
|
18
|
+
* changed across this load → the `contract` signal fires and the new value is
|
|
19
|
+
* stored. So a returning user whose app was rebuilt against a new schema is
|
|
20
|
+
* told to reload.
|
|
21
|
+
*
|
|
22
|
+
* `updateAvailable` is simply `bundle || contract` — either signal means "an
|
|
23
|
+
* update is available".
|
|
24
|
+
*
|
|
25
|
+
* ## Scope: load-time contract detection only (documented follow-up)
|
|
26
|
+
*
|
|
27
|
+
* Contract detection here fires at MOST once per load (the compare-on-init).
|
|
28
|
+
* LIVE-while-open contract detection — learning mid-session that the server
|
|
29
|
+
* redeployed a new contract, via the `_events` SSE manifest frame — pushes the
|
|
30
|
+
* same sticky `contract` signal through {@link UpdateState.notifyContractUpdated}.
|
|
31
|
+
*
|
|
32
|
+
* ## Durable last-seen storage + logout wipe
|
|
33
|
+
*
|
|
34
|
+
* The last-seen hash lives in the version-meta store
|
|
35
|
+
* (update-state/meta-store.ts) under the durable-store namespace, registered as
|
|
36
|
+
* a durable resource so a logout {@link wipeDurableStore} clears it too (the AC
|
|
37
|
+
* "wipe clears the last-seen-hash record"). If IndexedDB is unavailable the
|
|
38
|
+
* primitive still works for the bundle signal; contract detection degrades to a
|
|
39
|
+
* no-op (no durable baseline to compare against) rather than throwing.
|
|
40
|
+
*
|
|
41
|
+
* Engine-free (no `@tanstack/*` import) and framework-free (no Svelte/DOM) — the
|
|
42
|
+
* reactive binding is a separate smrt-svelte adapter (#1764 PART E).
|
|
43
|
+
*/
|
|
44
|
+
import { type DurableStoreKey } from './durable-store.js';
|
|
45
|
+
/** The two independent update signals plus their derived union. */
|
|
46
|
+
export interface UpdateAvailableState {
|
|
47
|
+
/** The client bundle changed on the server (SvelteKit `updated`). */
|
|
48
|
+
readonly bundle: boolean;
|
|
49
|
+
/** The API contract (manifest hash) changed across this load. */
|
|
50
|
+
readonly contract: boolean;
|
|
51
|
+
/** `bundle || contract` — either means an update is available. */
|
|
52
|
+
readonly updateAvailable: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** Configuration for {@link createUpdateState}. */
|
|
55
|
+
export interface UpdateStateConfig {
|
|
56
|
+
/**
|
|
57
|
+
* The RUNNING build's web-collection shape digest — the `manifestHash`
|
|
58
|
+
* constant the generated `@happyvertical/smrt-virt-web` module exports
|
|
59
|
+
* (#1764). Compared against the persisted last-seen value to detect a contract
|
|
60
|
+
* change across loads. Omit to disable contract detection (bundle-only).
|
|
61
|
+
*/
|
|
62
|
+
manifestHash?: string;
|
|
63
|
+
/**
|
|
64
|
+
* The durable-store identity the last-seen manifest hash is namespaced under —
|
|
65
|
+
* folds api base / tenant / identity (and, yes, the manifest hash itself, but
|
|
66
|
+
* see below). Shared with persistence/outbox so one {@link wipeDurableStore}
|
|
67
|
+
* clears the last-seen record too.
|
|
68
|
+
*
|
|
69
|
+
* NOTE the namespace's OWN `manifestHash` segment should be a STABLE value
|
|
70
|
+
* (e.g. the app's api-version or a constant), NOT the running build hash —
|
|
71
|
+
* otherwise a contract change would move the namespace and the compare would
|
|
72
|
+
* never find the prior value (always "first run"). Contract change is detected
|
|
73
|
+
* by the STORED VALUE under a stable namespace, not by the namespace moving.
|
|
74
|
+
*/
|
|
75
|
+
namespace: DurableStoreKey;
|
|
76
|
+
}
|
|
77
|
+
/** The framework-free update-available primitive. */
|
|
78
|
+
export interface UpdateState {
|
|
79
|
+
/** The current signal state (a fresh immutable snapshot). */
|
|
80
|
+
get(): UpdateAvailableState;
|
|
81
|
+
/**
|
|
82
|
+
* Subscribe to state changes; the callback fires immediately with the current
|
|
83
|
+
* state and again on every transition. Returns an unsubscribe function.
|
|
84
|
+
*/
|
|
85
|
+
subscribe(callback: (state: UpdateAvailableState) => void): () => void;
|
|
86
|
+
/**
|
|
87
|
+
* Push the BUNDLE signal — call from the consumer/smrt-svelte binding when
|
|
88
|
+
* SvelteKit's `updated` store flips true. Idempotent: once set it stays set
|
|
89
|
+
* (a bundle update does not un-happen).
|
|
90
|
+
*/
|
|
91
|
+
notifyBundleUpdated(): void;
|
|
92
|
+
/**
|
|
93
|
+
* Push the API CONTRACT signal — call when a live transport observes a server
|
|
94
|
+
* `manifestHash` that differs from this running build (#1859). Idempotent:
|
|
95
|
+
* once set it stays set (a contract mismatch does not un-happen).
|
|
96
|
+
*/
|
|
97
|
+
notifyContractUpdated(): void;
|
|
98
|
+
/**
|
|
99
|
+
* Resolves once the async contract detection has settled (compared the running
|
|
100
|
+
* hash against the persisted last-seen value and fired the signal if needed).
|
|
101
|
+
* Primarily for tests/SSR; UI code just subscribes.
|
|
102
|
+
*/
|
|
103
|
+
readonly ready: Promise<void>;
|
|
104
|
+
/** Detach the durable resource (does NOT wipe — that's wipeDurableStore). */
|
|
105
|
+
dispose(): void;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Create the framework-free `updateAvailable` primitive. Kicks off async
|
|
109
|
+
* contract detection immediately (compare running vs. persisted manifest hash);
|
|
110
|
+
* {@link UpdateState.notifyBundleUpdated} feeds the bundle signal, and
|
|
111
|
+
* {@link UpdateState.notifyContractUpdated} feeds the live contract signal. A
|
|
112
|
+
* change to EITHER signal notifies subscribers.
|
|
113
|
+
*/
|
|
114
|
+
export declare function createUpdateState(config: UpdateStateConfig): UpdateState;
|
|
115
|
+
//# sourceMappingURL=update-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-state.d.ts","sourceRoot":"","sources":["../src/update-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,OAAO,EACL,KAAK,eAAe,EAGrB,MAAM,oBAAoB,CAAC;AAO5B,mEAAmE;AACnE,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,kEAAkE;IAClE,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CACnC;AAED,mDAAmD;AACnD,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;OAWG;IACH,SAAS,EAAE,eAAe,CAAC;CAC5B;AAED,qDAAqD;AACrD,MAAM,WAAW,WAAW;IAC1B,6DAA6D;IAC7D,GAAG,IAAI,oBAAoB,CAAC;IAC5B;;;OAGG;IACH,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACvE;;;;OAIG;IACH,mBAAmB,IAAI,IAAI,CAAC;IAC5B;;;;OAIG;IACH,qBAAqB,IAAI,IAAI,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,6EAA6E;IAC7E,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG,WAAW,CAoHxE"}
|