@danxbot/ui 3.7.0 → 3.8.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/dist/index.js +11304 -10078
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +3 -0
- package/dist/types/lib/reads/hooks.d.ts +49 -0
- package/dist/types/lib/reads/index.d.ts +4 -0
- package/dist/types/lib/reads/refetch.d.ts +52 -0
- package/dist/types/lib/reads/slots.d.ts +52 -0
- package/dist/types/lib/reads/types.d.ts +139 -0
- package/dist/types/lib/realtime/hooks.d.ts +38 -0
- package/dist/types/lib/realtime/index.d.ts +4 -0
- package/dist/types/lib/realtime/provider.d.ts +38 -0
- package/dist/types/lib/realtime/registry.d.ts +172 -0
- package/dist/types/lib/realtime/types.d.ts +202 -0
- package/dist/types/lib/records/define-record-type.d.ts +29 -0
- package/dist/types/lib/records/hooks.d.ts +46 -0
- package/dist/types/lib/records/index.d.ts +5 -0
- package/dist/types/lib/records/store.d.ts +117 -0
- package/dist/types/lib/records/types.d.ts +188 -0
- package/dist/types/lib/records/wire.d.ts +96 -0
- package/package.json +1 -1
package/dist/types/index.d.ts
CHANGED
|
@@ -110,3 +110,6 @@ export { UseCaseCard, RoadmapItemCard, RoadmapSwimlaneBoard, UseCaseLedgerPage,
|
|
|
110
110
|
export { ProvenanceDisclosure, ProvenanceBadge, ConfidenceMeter, provenanceStateLabel, confidenceBand, type ProvenanceRecord, type ProvenanceState, type ProvenanceSource, type ProvenanceCheck, type ProvenanceCall, type ProvenanceSignoff, type ConfidenceBand, type ProvenanceDisclosureProps, type ProvenanceTriggerBadge, type ProvenanceBadgeProps, type ProvenanceBadgeAppearance, type ConfidenceMeterProps, } from "./components/provenance";
|
|
111
111
|
export { createSimStore, resetSimStores, useSimSettings, setSimSettings, getNetworkSpeed, getDataSource, simulateLatency, SimNetworkError, type SimStore, type SimStoreOptions, type SimSettings, type NetworkSpeed, type DataSource, type SimNamespace, } from "./lib/sim";
|
|
112
112
|
export { NodeCanvas, LAYOUT_DEFAULTS, layoutTree, pathToNode, type NodeCanvasProps, type CanvasEdge, type CanvasLayout, type CanvasNode, type CanvasOrientation, type LayoutOptions, type NodeRenderContext, type NodeRenderer, type PlacedNode, } from "./components/node-canvas";
|
|
113
|
+
export { defineRecordType, resetRecordTypes, useRecord, useRecords, useStaleness, write, writeAll, peekRecord, readState, readStaleness, subscribeToKey, subscribeToStaleness, retainKeys, sweepRecords, setRecordStoreTenant, getRecordStoreTenant, flushRecordNotifications, resetRecordStore, recordStoreStats, UNKNOWN_STATE, normalizeTypeName, normalizeTimestamp, recordKey, isRecordRef, type RecordRef, type ReadOptions, type RecordPayload, type RecordState, type RecordTypeOptions, type RecordTypeToken, type StalenessEntry, type WriteAllResult, type WriteOptions, type WriteResult, type WriteSource, type WriteVerdict, } from "./lib/records";
|
|
114
|
+
export { RealtimeProvider, useRealtime, useRealtimeEpoch, useRealtimeHealth, useRealtimeRegistry, SubscriptionRegistry, normalizeScope, scopeKey, readRealtimeEpoch, subscribeToRealtimeEpoch, subscribeToRealtimeEvents, resetRealtimeSignals, type RealtimeChannel, type RealtimeConnectionState, type RealtimeDiagnostic, type RealtimeEvent, type RealtimeHealth, type RealtimeProviderProps, type RealtimeScope, type RealtimeScopeInput, type RealtimeStatus, type RealtimeTransport, type RegistryOptions, type SubscriptionHealth, type SubscriptionLease, type SubscriptionRequest, } from "./lib/realtime";
|
|
115
|
+
export { useRecordRead, useRecordListRead, useRecordStoreSweep, applyMutationResult, acquireSlot, appendCreatedKey, emptySlotSnapshot, readSlot, resetSlots, slotKeys, subscribeToSlot, writeSlot, RefetchRunner, registerActiveRead, resetActiveReads, type ActiveRead, type ListFetch, type ListFetchResult, type ListReadOptions, type ListReadResult, type ReadError, type RecordFetch, type RecordReadOptions, type RecordReadResult, type RunnerOptions, type SlotSnapshot, } from "./lib/reads";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type RecordPayload, type RecordTypeToken, type WriteResult } from "../records";
|
|
2
|
+
import type { ListReadOptions, ListReadResult, RecordReadOptions, RecordReadResult } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Read one record: fetch it, write it into the store tagged with the field set
|
|
5
|
+
* it asked for, and return the store's answer.
|
|
6
|
+
*
|
|
7
|
+
* `id` may be null for a conditional read — the hook then fetches nothing and
|
|
8
|
+
* reports `unknown`, rather than forcing a caller around a rule of hooks.
|
|
9
|
+
*/
|
|
10
|
+
export declare function useRecordRead<T>(token: RecordTypeToken<T>, id: string | number | null | undefined, options: RecordReadOptions): RecordReadResult<T>;
|
|
11
|
+
/**
|
|
12
|
+
* Read a page: fetch it, write every row into the store, and record which keys
|
|
13
|
+
* answered the query in a LIST SLOT.
|
|
14
|
+
*
|
|
15
|
+
* The slot is what makes a `created` broadcast visible and a tombstone
|
|
16
|
+
* removable without a fetch — and it is where `meta` lives, because pagination
|
|
17
|
+
* has no identity and can never be a record.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useRecordListRead<T, TMeta = unknown>(token: RecordTypeToken<T>, options: ListReadOptions<TMeta>): ListReadResult<T, TMeta>;
|
|
20
|
+
/**
|
|
21
|
+
* Write a mutation's returned item into the store.
|
|
22
|
+
*
|
|
23
|
+
* A MUTATION RESPONSE IS A FETCH, and this exists because that is a question an
|
|
24
|
+
* implementer will otherwise answer by coin flip. It is a full server answer to
|
|
25
|
+
* a request this client made: it may satisfy a field-set tag, it clears
|
|
26
|
+
* staleness on every field it carries, and it must NEVER be dropped by the
|
|
27
|
+
* self-echo filter — that filter applies to broadcasts, and dropping a
|
|
28
|
+
* mutation's own response would discard the server-computed fields the caller
|
|
29
|
+
* asked for. Structurally it cannot be dropped, because the filter lives on the
|
|
30
|
+
* broadcast path and this does not go through it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function applyMutationResult(payload: RecordPayload, options: {
|
|
33
|
+
tag: string;
|
|
34
|
+
tenant?: string | number;
|
|
35
|
+
}): WriteResult;
|
|
36
|
+
/**
|
|
37
|
+
* Sweep unreachable records on a bounded schedule, for as long as this is
|
|
38
|
+
* mounted.
|
|
39
|
+
*
|
|
40
|
+
* IT IS A HOOK, NOT AN INTERVAL STARTED ON IMPORT. A design-system module that
|
|
41
|
+
* schedules work because somebody imported it is a worse defect than the leak
|
|
42
|
+
* it prevents — it runs in every consumer, including ones with no store at all.
|
|
43
|
+
* So the library exports the sweep and the CONSUMER decides when: this hook at
|
|
44
|
+
* an app root, a call at a navigation boundary, or both. A navigation boundary
|
|
45
|
+
* alone is not enough, because a broadcast can create a key no root reaches and
|
|
46
|
+
* a screen nobody navigates away from accumulates those for as long as the tab
|
|
47
|
+
* is open.
|
|
48
|
+
*/
|
|
49
|
+
export declare function useRecordStoreSweep(intervalMs?: number): void;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { useRecordRead, useRecordListRead, applyMutationResult, useRecordStoreSweep } from "./hooks";
|
|
2
|
+
export { acquireSlot, appendCreatedKey, emptySlotSnapshot, readSlot, resetSlots, slotKeys, subscribeToSlot, writeSlot, type SlotSnapshot, } from "./slots";
|
|
3
|
+
export { RefetchRunner, registerActiveRead, resetActiveReads, type ActiveRead, type RunnerOptions } from "./refetch";
|
|
4
|
+
export type { ListFetch, ListFetchResult, ListReadOptions, ListReadResult, ReadError, RecordFetch, RecordReadOptions, RecordReadResult, } from "./types";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface RunnerOptions {
|
|
2
|
+
readonly minIntervalMs?: number;
|
|
3
|
+
/** Test seam. Defaults to `Date.now`. */
|
|
4
|
+
readonly now?: () => number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* One read's refetch discipline: a floor between runs, and one request in
|
|
8
|
+
* flight at a time.
|
|
9
|
+
*
|
|
10
|
+
* A SECOND SIGNAL ARRIVING MID-FLIGHT SETS A RE-RUN FLAG rather than issuing a
|
|
11
|
+
* second request. Two overlapping refetches of the same target are not twice as
|
|
12
|
+
* fresh; they are two answers racing to write, and the loser can be the newer
|
|
13
|
+
* one.
|
|
14
|
+
*/
|
|
15
|
+
export declare class RefetchRunner {
|
|
16
|
+
private readonly run;
|
|
17
|
+
private inFlight;
|
|
18
|
+
private rerun;
|
|
19
|
+
private lastFinishedAt;
|
|
20
|
+
private timer;
|
|
21
|
+
private disposed;
|
|
22
|
+
private readonly minIntervalMs;
|
|
23
|
+
private readonly now;
|
|
24
|
+
constructor(run: () => Promise<void>, options?: RunnerOptions);
|
|
25
|
+
/** Ask for a refetch. Respects the floor and the in-flight request. */
|
|
26
|
+
request(): void;
|
|
27
|
+
/**
|
|
28
|
+
* Run now, floor and all.
|
|
29
|
+
*
|
|
30
|
+
* For the FIRST fetch and for a key change — neither is a refetch, and making
|
|
31
|
+
* a fresh screen wait out an interval that exists to damp a broadcast stream
|
|
32
|
+
* would be the floor working against its own purpose.
|
|
33
|
+
*/
|
|
34
|
+
runNow(): void;
|
|
35
|
+
private execute;
|
|
36
|
+
dispose(): void;
|
|
37
|
+
}
|
|
38
|
+
/** One mounted read, as the driver sees it. */
|
|
39
|
+
export interface ActiveRead {
|
|
40
|
+
/** The keys this read is responsible for. Re-read per signal: a list's move. */
|
|
41
|
+
roots(): readonly string[];
|
|
42
|
+
/** Ask this read to re-issue the request it already made. */
|
|
43
|
+
request(): void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Register a read so staleness and liveness recovery can reach it.
|
|
47
|
+
*
|
|
48
|
+
* @returns unregister
|
|
49
|
+
*/
|
|
50
|
+
export declare function registerActiveRead(read: ActiveRead): () => void;
|
|
51
|
+
/** Test seam. Nothing in the library calls this. */
|
|
52
|
+
export declare function resetActiveReads(): void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface SlotSnapshot<T = unknown, TMeta = unknown> {
|
|
2
|
+
readonly items: readonly T[];
|
|
3
|
+
readonly pending: readonly T[];
|
|
4
|
+
readonly pendingCount: number;
|
|
5
|
+
readonly meta: TMeta | undefined;
|
|
6
|
+
/** Every key the slot holds, fetched and pending. The refetch roots. */
|
|
7
|
+
readonly keys: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
/** `getServerSnapshot`, and the answer for a slot nothing has written yet. */
|
|
10
|
+
export declare function emptySlotSnapshot(): SlotSnapshot;
|
|
11
|
+
/**
|
|
12
|
+
* Claim a slot for the duration of the returned release.
|
|
13
|
+
*
|
|
14
|
+
* REFCOUNTED AND IDEMPOTENT, which is what makes StrictMode's mount / unmount /
|
|
15
|
+
* mount leave the slot — and its retained keys — exactly as it found them.
|
|
16
|
+
*/
|
|
17
|
+
export declare function acquireSlot(id: string, options: {
|
|
18
|
+
type: string;
|
|
19
|
+
tag: string;
|
|
20
|
+
acceptsCreated: boolean;
|
|
21
|
+
}): () => void;
|
|
22
|
+
/**
|
|
23
|
+
* Replace a slot's membership with a fetch's answer.
|
|
24
|
+
*
|
|
25
|
+
* A key that arrived through a `created` event and is now in the fetched page
|
|
26
|
+
* stops being pending — the fetch has placed it, which is the only thing that
|
|
27
|
+
* ever could.
|
|
28
|
+
*/
|
|
29
|
+
export declare function writeSlot(id: string, keys: readonly string[], meta: unknown): void;
|
|
30
|
+
/**
|
|
31
|
+
* Append a record that entered the set elsewhere.
|
|
32
|
+
*
|
|
33
|
+
* WITH NO POSITION. The server owns sort order and page boundary; a client that
|
|
34
|
+
* inserts at a guessed index renders a list that is subtly, confidently wrong.
|
|
35
|
+
*/
|
|
36
|
+
export declare function appendCreatedKey(type: string, key: string): void;
|
|
37
|
+
export declare function subscribeToSlot(id: string, listener: () => void): () => void;
|
|
38
|
+
/** The keys a slot holds right now — the roots a targeted refetch matches on. */
|
|
39
|
+
export declare function slotKeys(id: string): readonly string[];
|
|
40
|
+
/**
|
|
41
|
+
* The slot's projection, memoized to a stable reference.
|
|
42
|
+
*
|
|
43
|
+
* IT RECOMPUTES AND THEN COMPARES rather than trusting its dirty flag, because
|
|
44
|
+
* a member notification does not always change what this slot renders — a
|
|
45
|
+
* record whose field-set tag is still unsatisfied moves nothing here — and
|
|
46
|
+
* handing back a fresh array for an unchanged list re-renders every consumer
|
|
47
|
+
* for nothing. The elements themselves are the store's own reference-stable
|
|
48
|
+
* snapshots, so comparing them is one identity check each.
|
|
49
|
+
*/
|
|
50
|
+
export declare function readSlot(id: string): SlotSnapshot;
|
|
51
|
+
/** Test seam. Nothing in the library calls this. */
|
|
52
|
+
export declare function resetSlots(): void;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { RecordPayload, RecordState } from "../records";
|
|
2
|
+
/**
|
|
3
|
+
* A refusal.
|
|
4
|
+
*
|
|
5
|
+
* IT LIVES HERE AND NOT IN THE STORE, deliberately. A 404 is not one of the
|
|
6
|
+
* store's record states, because a refusal carries a message and a remedy the
|
|
7
|
+
* store has no way to build — so a `404` member there would be permanently
|
|
8
|
+
* unreachable. It belongs to the layer that performed the request, which is
|
|
9
|
+
* this one.
|
|
10
|
+
*/
|
|
11
|
+
export interface ReadError {
|
|
12
|
+
readonly message: string;
|
|
13
|
+
/** Whatever was thrown. The app's own error wrapper reads it. */
|
|
14
|
+
readonly cause: unknown;
|
|
15
|
+
}
|
|
16
|
+
/** What a fetch of one record must resolve to: the payload itself. */
|
|
17
|
+
export type RecordFetch = (signal?: AbortSignal) => Promise<RecordPayload>;
|
|
18
|
+
/** What a fetch of a page must resolve to. */
|
|
19
|
+
export interface ListFetchResult<TMeta = unknown> {
|
|
20
|
+
readonly items: readonly RecordPayload[];
|
|
21
|
+
/**
|
|
22
|
+
* Pagination, totals, anything the page carries that is NOT a record.
|
|
23
|
+
*
|
|
24
|
+
* IT LIVES IN THE SLOT BECAUSE IT CANNOT LIVE ANYWHERE ELSE. `meta.total` has
|
|
25
|
+
* no `__type` and no `id`, so it can never be a record — and a normalized
|
|
26
|
+
* store without a place for it is the half of this design that gets omitted
|
|
27
|
+
* and then discovered from a screen that cannot render its own page count.
|
|
28
|
+
*/
|
|
29
|
+
readonly meta?: TMeta;
|
|
30
|
+
}
|
|
31
|
+
export type ListFetch<TMeta = unknown> = (signal?: AbortSignal) => Promise<ListFetchResult<TMeta>>;
|
|
32
|
+
/** Options common to both reads. */
|
|
33
|
+
interface CommonReadOptions {
|
|
34
|
+
/**
|
|
35
|
+
* The opaque field-set name this request asks for — `"card"`, `"detail"`,
|
|
36
|
+
* whatever the app's own api layer already calls its selector.
|
|
37
|
+
*
|
|
38
|
+
* The library never interprets it. It is compared for equality and nothing
|
|
39
|
+
* else, which is what lets `needs` gate a read on a completion the library
|
|
40
|
+
* cannot compute.
|
|
41
|
+
*/
|
|
42
|
+
readonly tag: string;
|
|
43
|
+
/**
|
|
44
|
+
* The tenant to stamp writes with. Defaults to the store's current tenant,
|
|
45
|
+
* READ AT REQUEST TIME.
|
|
46
|
+
*
|
|
47
|
+
* Request time is the whole point: stamping on ARRIVAL reads the tenant as it
|
|
48
|
+
* is now, which defeats the check entirely — a team-A response that resolves
|
|
49
|
+
* after a switch to team B would be stamped B and merged into B's store.
|
|
50
|
+
*/
|
|
51
|
+
readonly tenant?: string | number;
|
|
52
|
+
/** Skip the fetch entirely. The read reports `loading: false`. */
|
|
53
|
+
readonly enabled?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* The floor between two refetches of THIS read. Default 1000ms.
|
|
56
|
+
*
|
|
57
|
+
* A TIME FLOOR IS NOT OPTIONAL, and breadth-coalescing alone is not enough:
|
|
58
|
+
* at least one real broadcast source has no change guard at all and fires on
|
|
59
|
+
* every related save, so during a long job it is a continuous STREAM rather
|
|
60
|
+
* than a burst. Without a floor that is one refetch per frame, of whatever
|
|
61
|
+
* the heaviest read on the screen happens to be.
|
|
62
|
+
*/
|
|
63
|
+
readonly minRefetchIntervalMs?: number;
|
|
64
|
+
}
|
|
65
|
+
export interface RecordReadOptions extends CommonReadOptions {
|
|
66
|
+
readonly fetch: RecordFetch;
|
|
67
|
+
}
|
|
68
|
+
export interface ListReadOptions<TMeta = unknown> extends CommonReadOptions {
|
|
69
|
+
/**
|
|
70
|
+
* The serialized query — the slot's identity.
|
|
71
|
+
*
|
|
72
|
+
* Two components asking the same question share one slot; two different
|
|
73
|
+
* questions never collide. It is the caller's serialization because only the
|
|
74
|
+
* caller knows which of its parameters are part of the question.
|
|
75
|
+
*/
|
|
76
|
+
readonly key: string;
|
|
77
|
+
readonly fetch: ListFetch<TMeta>;
|
|
78
|
+
/**
|
|
79
|
+
* Whether a `created` broadcast for this type may append to this slot.
|
|
80
|
+
* Default `true`.
|
|
81
|
+
*
|
|
82
|
+
* SET IT FALSE FOR A SLOT WHOSE MEMBERSHIP THIS CLIENT CANNOT VERIFY. The
|
|
83
|
+
* backend evaluates a filter subscription server-side per save, so a
|
|
84
|
+
* `created` event arriving on such a subscription IS authoritative that the
|
|
85
|
+
* record matches — but the library cannot tell WHICH of several slots of the
|
|
86
|
+
* same type it matches when a screen holds more than one differently-filtered
|
|
87
|
+
* list. Appending to all of them would be a guess; this flag is where the app
|
|
88
|
+
* says which one owns the event.
|
|
89
|
+
*/
|
|
90
|
+
readonly acceptsCreated?: boolean;
|
|
91
|
+
}
|
|
92
|
+
/** The loading / refreshing / error triple, shared by both reads. */
|
|
93
|
+
interface ReadStatus {
|
|
94
|
+
/** No answer yet AND a fetch is in flight. This is the skeleton. */
|
|
95
|
+
readonly loading: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* An answer is on screen AND a fetch is in flight.
|
|
98
|
+
*
|
|
99
|
+
* SEPARATE FROM `loading` ON PURPOSE. Collapsing the two blanks a populated
|
|
100
|
+
* screen every time anything invalidates — which is exactly what recovering
|
|
101
|
+
* from staleness by blanking the screen would do, and it is a worse
|
|
102
|
+
* experience than the staleness.
|
|
103
|
+
*/
|
|
104
|
+
readonly refreshing: boolean;
|
|
105
|
+
readonly error: ReadError | undefined;
|
|
106
|
+
/** Re-issue the request now, subject to the refetch floor. */
|
|
107
|
+
readonly refetch: () => void;
|
|
108
|
+
}
|
|
109
|
+
export interface RecordReadResult<T> extends ReadStatus {
|
|
110
|
+
/** The store's answer: `unknown`, `present` or `deleted`. Never a maybe. */
|
|
111
|
+
readonly state: RecordState<T>;
|
|
112
|
+
/** `state.record` when present, for the common case. */
|
|
113
|
+
readonly data: T | undefined;
|
|
114
|
+
}
|
|
115
|
+
export interface ListReadResult<T, TMeta = unknown> extends ReadStatus {
|
|
116
|
+
/** The fetched page, in the server's order, tombstoned rows removed. */
|
|
117
|
+
readonly items: readonly T[];
|
|
118
|
+
/**
|
|
119
|
+
* Rows that entered the set through a `created` broadcast, in arrival order.
|
|
120
|
+
*
|
|
121
|
+
* THEY ARE NOT SPLICED INTO `items`, and that is the honest form of the rule
|
|
122
|
+
* rather than a limitation: sort position and page boundary are decided by
|
|
123
|
+
* the server, so guessing where a new row goes is precisely the lie the rule
|
|
124
|
+
* exists to prevent. A screen renders "N new" and the next fetch places them.
|
|
125
|
+
*/
|
|
126
|
+
readonly pending: readonly T[];
|
|
127
|
+
/**
|
|
128
|
+
* How many rows are waiting, INCLUDING ones not yet renderable.
|
|
129
|
+
*
|
|
130
|
+
* THIS IS NOT `pending.length`, and the difference is the whole reason it
|
|
131
|
+
* exists. A `created` broadcast is partial by whitelist, so it can never
|
|
132
|
+
* satisfy the field-set tag — meaning the appended record is countable but
|
|
133
|
+
* not renderable until a fetch fills it in. Reading "N new" off `pending`
|
|
134
|
+
* would print zero for exactly the case the marker was added to cover.
|
|
135
|
+
*/
|
|
136
|
+
readonly pendingCount: number;
|
|
137
|
+
readonly meta: TMeta | undefined;
|
|
138
|
+
}
|
|
139
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { RecordTypeToken } from "../records";
|
|
2
|
+
import type { RealtimeHealth, RealtimeScopeInput } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Declare that this component is looking at these records and wants these
|
|
5
|
+
* events.
|
|
6
|
+
*
|
|
7
|
+
* COALESCED. Twenty-five components asking for the same scope produce ONE
|
|
8
|
+
* server-side registration, and the union of everyone's event names is bound —
|
|
9
|
+
* never the first caller's only, which is the live defect this module was
|
|
10
|
+
* written against.
|
|
11
|
+
*
|
|
12
|
+
* NO-OP WITHOUT A PROVIDER. An application with realtime switched off keeps
|
|
13
|
+
* fetching and keeps rendering; nothing here throws.
|
|
14
|
+
*
|
|
15
|
+
* @param token the record type, from `defineRecordType`
|
|
16
|
+
* @param scope `"all"`, an id list, or `{filter}` the server evaluates
|
|
17
|
+
* @param events event names to bind, e.g. `["created", "updated", "deleted"]`
|
|
18
|
+
*/
|
|
19
|
+
export declare function useRealtime<T>(token: RecordTypeToken<T>, scope: RealtimeScopeInput, events: readonly string[]): void;
|
|
20
|
+
/**
|
|
21
|
+
* The renderable liveness state.
|
|
22
|
+
*
|
|
23
|
+
* IT IS RENDERABLE ON PURPOSE. A dead subscription is indistinguishable from a
|
|
24
|
+
* quiet one, and the prior art's answer — a console panel behind a keyboard
|
|
25
|
+
* shortcut — is why two real bugs in it went unnoticed for years. This is the
|
|
26
|
+
* equivalent of `refreshing`: a word the reader can see, not a log line.
|
|
27
|
+
*/
|
|
28
|
+
export declare function useRealtimeHealth(): RealtimeHealth;
|
|
29
|
+
/**
|
|
30
|
+
* The "you may have missed events" counter, for a consumer that wants to fold
|
|
31
|
+
* it into its own invalidation rather than use the read binding.
|
|
32
|
+
*
|
|
33
|
+
* IT IS A REVISION, NEVER A READ KEY. A key change blanks the screen to a
|
|
34
|
+
* skeleton; a revision keeps the answer on screen and marks it refreshing. This
|
|
35
|
+
* signal exists because a subscription went dead and came back — recovering
|
|
36
|
+
* from that by blanking the screen is a worse experience than the staleness.
|
|
37
|
+
*/
|
|
38
|
+
export declare function useRealtimeEpoch(): number;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { RealtimeProvider, useRealtimeRegistry, type RealtimeProviderProps } from "./provider";
|
|
2
|
+
export { useRealtime, useRealtimeEpoch, useRealtimeHealth } from "./hooks";
|
|
3
|
+
export { SubscriptionRegistry, normalizeScope, scopeKey, readRealtimeEpoch, subscribeToRealtimeEpoch, subscribeToRealtimeEvents, resetRealtimeSignals, type RegistryOptions, } from "./registry";
|
|
4
|
+
export type { RealtimeChannel, RealtimeConnectionState, RealtimeDiagnostic, RealtimeEvent, RealtimeHealth, RealtimeScope, RealtimeScopeInput, RealtimeStatus, RealtimeTransport, SubscriptionHealth, SubscriptionLease, SubscriptionRequest, } from "./types";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { SubscriptionRegistry, type RegistryOptions } from "./registry";
|
|
3
|
+
export interface RealtimeProviderProps extends Omit<RegistryOptions, "transport" | "tenant" | "now"> {
|
|
4
|
+
/**
|
|
5
|
+
* Everything environment-specific in one object: socket, endpoints,
|
|
6
|
+
* connection observer. INJECTED, never imported — see `types.ts` for why, and
|
|
7
|
+
* for the one constraint on its implementation that a type cannot express.
|
|
8
|
+
*/
|
|
9
|
+
readonly transport: RegistryOptions["transport"];
|
|
10
|
+
/**
|
|
11
|
+
* The team, org or account these records belong to.
|
|
12
|
+
*
|
|
13
|
+
* CHANGING IT IS A REAL EVENT, not a re-render: every server-side
|
|
14
|
+
* registration is dropped, every channel is left, the record store is
|
|
15
|
+
* cleared, and everything still mounted re-subscribes under the new tenant.
|
|
16
|
+
*/
|
|
17
|
+
readonly tenant?: string | number;
|
|
18
|
+
/**
|
|
19
|
+
* Whether this provider owns the record store's tenant. Default `true`.
|
|
20
|
+
*
|
|
21
|
+
* Set it false only when something else already calls
|
|
22
|
+
* `setRecordStoreTenant` — two writers of one value is how they disagree.
|
|
23
|
+
*/
|
|
24
|
+
readonly ownsStoreTenant?: boolean;
|
|
25
|
+
readonly children: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
export declare function RealtimeProvider({ transport, tenant, ownsStoreTenant, children, ...options }: RealtimeProviderProps): import("react").JSX.Element;
|
|
28
|
+
/**
|
|
29
|
+
* The registry, or `null` when no provider is mounted.
|
|
30
|
+
*
|
|
31
|
+
* THIS PACKAGE'S USUAL CONVENTION IS INVERTED HERE, DELIBERATELY. Every other
|
|
32
|
+
* provider in this library throws when its hook is used outside it, and that is
|
|
33
|
+
* right for a capability a component cannot work without. Realtime is not one:
|
|
34
|
+
* an application with no transport configured must degrade to plain fetching,
|
|
35
|
+
* never crash. A design system that hard-fails because an OPTIONAL capability
|
|
36
|
+
* is absent is not offering an optional capability.
|
|
37
|
+
*/
|
|
38
|
+
export declare function useRealtimeRegistry(): SubscriptionRegistry | null;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { RealtimeDiagnostic, RealtimeEvent, RealtimeHealth, RealtimeScope, RealtimeScopeInput, RealtimeTransport } from "./types";
|
|
2
|
+
import type { RecordPayload } from "../records";
|
|
3
|
+
export interface RegistryOptions {
|
|
4
|
+
readonly transport: RealtimeTransport;
|
|
5
|
+
readonly tenant?: string | number;
|
|
6
|
+
readonly keepaliveIntervalMs?: number;
|
|
7
|
+
readonly expiryMarginMs?: number;
|
|
8
|
+
readonly baseBackoffMs?: number;
|
|
9
|
+
readonly maxBackoffMs?: number;
|
|
10
|
+
readonly maxAttempts?: number;
|
|
11
|
+
/**
|
|
12
|
+
* The event name that means "this record just entered the set".
|
|
13
|
+
*
|
|
14
|
+
* Configurable because the name is the backend's, not this library's, and
|
|
15
|
+
* hard-coding one would be the only domain fact in this module.
|
|
16
|
+
*/
|
|
17
|
+
readonly createdEventName?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Drop a broadcast this client caused.
|
|
20
|
+
*
|
|
21
|
+
* A PREDICATE RATHER THAN A FIELD NAME, because the field that says who
|
|
22
|
+
* caused a change belongs to a wire format, not to a design system. Supplied
|
|
23
|
+
* by the app or absent; absent means every broadcast is written.
|
|
24
|
+
*
|
|
25
|
+
* IT APPLIES TO BROADCASTS ONLY, structurally — a fetch and a mutation's
|
|
26
|
+
* returned item never pass through here at all. That matters: a mutation
|
|
27
|
+
* response is self-caused BY DEFINITION, and dropping it would discard the
|
|
28
|
+
* server-computed fields the client asked for.
|
|
29
|
+
*/
|
|
30
|
+
readonly isSelfEvent?: (payload: RecordPayload) => boolean;
|
|
31
|
+
readonly onDiagnostic?: (diagnostic: RealtimeDiagnostic) => void;
|
|
32
|
+
/** Test seam. Defaults to `Date.now`. */
|
|
33
|
+
readonly now?: () => number;
|
|
34
|
+
}
|
|
35
|
+
export declare function readRealtimeEpoch(): number;
|
|
36
|
+
export declare function subscribeToRealtimeEpoch(listener: () => void): () => void;
|
|
37
|
+
/** Every broadcast, AFTER it has been written to the store. */
|
|
38
|
+
export declare function subscribeToRealtimeEvents(listener: (event: RealtimeEvent) => void): () => void;
|
|
39
|
+
/** Test seam. Nothing in the library calls this. */
|
|
40
|
+
export declare function resetRealtimeSignals(): void;
|
|
41
|
+
/**
|
|
42
|
+
* Normalize a caller's scope and give it a stable key.
|
|
43
|
+
*
|
|
44
|
+
* IDS ARE SORTED AND STRINGIFIED so two components asking for the same rows in
|
|
45
|
+
* a different order coalesce onto one subscription rather than opening two —
|
|
46
|
+
* and so a numeric id and its string form never split an entry, exactly as the
|
|
47
|
+
* store keys records.
|
|
48
|
+
*/
|
|
49
|
+
export declare function normalizeScope(input: RealtimeScopeInput): RealtimeScope;
|
|
50
|
+
export declare function scopeKey(scope: RealtimeScope): string;
|
|
51
|
+
export declare class SubscriptionRegistry {
|
|
52
|
+
private readonly transport;
|
|
53
|
+
private readonly entries;
|
|
54
|
+
private readonly channels;
|
|
55
|
+
private readonly options;
|
|
56
|
+
private readonly healthListeners;
|
|
57
|
+
private tenant;
|
|
58
|
+
private connection;
|
|
59
|
+
private stopConnectionObserver;
|
|
60
|
+
private timer;
|
|
61
|
+
private healthSnapshot;
|
|
62
|
+
private lastError;
|
|
63
|
+
private started;
|
|
64
|
+
private disposed;
|
|
65
|
+
constructor(options: RegistryOptions);
|
|
66
|
+
/**
|
|
67
|
+
* Begin observing the connection and renewing leases.
|
|
68
|
+
*
|
|
69
|
+
* THE TIMER STARTS HERE, NEVER ON IMPORT. A design-system module that starts
|
|
70
|
+
* an interval because somebody imported it is a worse defect than the leak it
|
|
71
|
+
* would be preventing: it runs in every consumer, including the ones that
|
|
72
|
+
* never touch realtime at all.
|
|
73
|
+
*/
|
|
74
|
+
start(): void;
|
|
75
|
+
/**
|
|
76
|
+
* Stop the timers. REVERSIBLE, and it keeps every entry and every channel.
|
|
77
|
+
*
|
|
78
|
+
* THIS IS WHAT STRICTMODE REQUIRES. React mounts every effect, unmounts it,
|
|
79
|
+
* and mounts it again in development — so a teardown that dropped the entries
|
|
80
|
+
* would make the remount issue a SECOND subscribe for a registration that is
|
|
81
|
+
* still perfectly live, and the whole misbehaviour would exist only in
|
|
82
|
+
* development, which is the worst place for a realtime bug to live because it
|
|
83
|
+
* is where the thing is being debugged.
|
|
84
|
+
*
|
|
85
|
+
* IT DOES NOT UNSUBSCRIBE, and it does not leave the socket channels. Unmount
|
|
86
|
+
* never touches the server-side registry — the entry lapses against its own
|
|
87
|
+
* TTL — and the socket belongs to the application that injected it. A tenant
|
|
88
|
+
* switch is the one place either of those happens, and it has its own method.
|
|
89
|
+
*/
|
|
90
|
+
stop(): void;
|
|
91
|
+
/**
|
|
92
|
+
* Full teardown: stop, leave every channel, forget every entry.
|
|
93
|
+
*
|
|
94
|
+
* NOT what a component unmount calls — see `stop`. This exists for an
|
|
95
|
+
* application deliberately switching realtime off, which is a decision no
|
|
96
|
+
* lifecycle can infer.
|
|
97
|
+
*/
|
|
98
|
+
dispose(): void;
|
|
99
|
+
/**
|
|
100
|
+
* Switch tenants: tear the server-side registrations down, clear the socket,
|
|
101
|
+
* and re-establish everything still mounted under the new tenant.
|
|
102
|
+
*
|
|
103
|
+
* THIS IS THE ONE PLACE `unsubscribe` IS CALLED. Never-unsubscribe applies to
|
|
104
|
+
* UNMOUNT, where tearing down can kill another tab's subscription; a
|
|
105
|
+
* subscription left lapsing on the PREVIOUS tenant's channel is a leak of a
|
|
106
|
+
* different kind, and one this library can close on its own.
|
|
107
|
+
*/
|
|
108
|
+
setTenant(tenant: string | number | undefined): void;
|
|
109
|
+
getTenant(): string | number | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* Declare interest in a (type, scope) with a set of event names.
|
|
112
|
+
*
|
|
113
|
+
* THE RETURNED RELEASE IS IDEMPOTENT AND EXACTLY PAIRED, which is what makes
|
|
114
|
+
* StrictMode's mount / unmount / mount leave exactly ONE subscription: the
|
|
115
|
+
* refcount goes 1, 0, 1 while the server-side registration is never dropped,
|
|
116
|
+
* so the remount reuses the live lease instead of issuing a second subscribe.
|
|
117
|
+
*/
|
|
118
|
+
acquire(type: string, scopeInput: RealtimeScopeInput, events: readonly string[]): () => void;
|
|
119
|
+
private subscribeEntry;
|
|
120
|
+
/**
|
|
121
|
+
* Bounded exponential backoff, then a terminal DEAD state.
|
|
122
|
+
*
|
|
123
|
+
* NOTHING IS ESCALATED, EVER. The failure that motivates this is a 401 on a
|
|
124
|
+
* background resubscribe: routed through an app's ordinary HTTP error path it
|
|
125
|
+
* clears the session and logs somebody out mid-screen. Here it is a number in
|
|
126
|
+
* `attempts`, a string in `lastError`, and a status a screen can print.
|
|
127
|
+
*/
|
|
128
|
+
private scheduleRetry;
|
|
129
|
+
private clearRetry;
|
|
130
|
+
/**
|
|
131
|
+
* Is this subscription failing to deliver?
|
|
132
|
+
*
|
|
133
|
+
* THREE INPUTS, ANY ONE SUFFICIENT, and the first is the one no reconnect
|
|
134
|
+
* hook can see: the lease simply runs out while everything else looks
|
|
135
|
+
* healthy.
|
|
136
|
+
*/
|
|
137
|
+
private isDead;
|
|
138
|
+
/**
|
|
139
|
+
* One pass: renew what is live, revive what is dead.
|
|
140
|
+
*
|
|
141
|
+
* Exposed so a consumer — or a test — can run it without waiting for the
|
|
142
|
+
* interval. Everything it does is idempotent.
|
|
143
|
+
*/
|
|
144
|
+
tick(): void;
|
|
145
|
+
private keepalive;
|
|
146
|
+
private onConnectionState;
|
|
147
|
+
private ensureChannel;
|
|
148
|
+
private leaveAllChannels;
|
|
149
|
+
/**
|
|
150
|
+
* One broadcast: filter, write, announce.
|
|
151
|
+
*
|
|
152
|
+
* THE TENANT IS THE ONE THE CHANNEL WAS JOINED UNDER, captured at bind time
|
|
153
|
+
* and never read at delivery time. Reading it on arrival reads the tenant as
|
|
154
|
+
* it is NOW, which is precisely how a payload from the team you just left
|
|
155
|
+
* lands in the store as the team you just joined.
|
|
156
|
+
*/
|
|
157
|
+
private deliver;
|
|
158
|
+
/** The event name that means "entered the set", for the read binding. */
|
|
159
|
+
get createdEventName(): string;
|
|
160
|
+
subscribeToHealth: (listener: () => void) => (() => void);
|
|
161
|
+
/**
|
|
162
|
+
* The renderable state, memoized so `getSnapshot` returns the identical
|
|
163
|
+
* object when nothing changed — the constraint every store in this package is
|
|
164
|
+
* written around.
|
|
165
|
+
*/
|
|
166
|
+
readHealth: () => RealtimeHealth;
|
|
167
|
+
private deriveStatus;
|
|
168
|
+
private notifyHealth;
|
|
169
|
+
private requestFor;
|
|
170
|
+
private callTransport;
|
|
171
|
+
private diagnose;
|
|
172
|
+
}
|