@danxbot/ui 7.4.3 → 7.6.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 +10341 -10137
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/lib/reads/index.d.ts +2 -1
- package/dist/types/lib/reads/optimistic.d.ts +87 -0
- package/dist/types/lib/reads/slots.d.ts +30 -0
- package/dist/types/lib/records/store.d.ts +92 -1
- package/package.json +1 -1
package/dist/types/index.d.ts
CHANGED
|
@@ -125,4 +125,4 @@ export { createSimStore, resetSimStores, useSimSettings, setSimSettings, getNetw
|
|
|
125
125
|
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";
|
|
126
126
|
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";
|
|
127
127
|
export { RealtimeProvider, useRealtime, useRealtimeEpoch, useRealtimeHealth, useRealtimeRegistry, SubscriptionRegistry, normalizeScope, scopeKey, subscriptionKey, 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";
|
|
128
|
-
export { useRecordRead, useRecordListRead, useRecordStoreSweep, applyMutationResult, acquireSlot, appendCreated, 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";
|
|
128
|
+
export { useRecordRead, useRecordListRead, useRecordStoreSweep, applyMutationResult, optimisticWrite, beginOptimisticWrite, resetOptimisticWrites, type OptimisticHandle, type OptimisticWriteOptions, type OptimisticWriteSpec, type SlotDescriptor, acquireSlot, appendCreated, 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";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { useRecordRead, useRecordListRead, applyMutationResult, useRecordStoreSweep } from "./hooks";
|
|
2
|
-
export { acquireSlot, appendCreated, emptySlotSnapshot, readSlot, resetSlots, slotKeys, subscribeToSlot, writeSlot, type SlotSnapshot, } from "./slots";
|
|
2
|
+
export { acquireSlot, appendCreated, emptySlotSnapshot, readSlot, resetSlots, slotKeys, subscribeToSlot, writeSlot, type SlotDescriptor, type SlotSnapshot, } from "./slots";
|
|
3
|
+
export { optimisticWrite, beginOptimisticWrite, resetOptimisticWrites, type OptimisticHandle, type OptimisticWriteOptions, type OptimisticWriteSpec, } from "./optimistic";
|
|
3
4
|
export { RefetchRunner, registerActiveRead, resetActiveReads, type ActiveRead, type RunnerOptions } from "./refetch";
|
|
4
5
|
export type { ListFetch, ListFetchResult, ListReadOptions, ListReadResult, ReadError, RecordFetch, RecordReadOptions, RecordReadResult, } from "./types";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { type RecordTypeToken } from "../records";
|
|
2
|
+
import { type SlotDescriptor } from "./slots";
|
|
3
|
+
/** Which records to change, and how. */
|
|
4
|
+
export type OptimisticWriteSpec<T> = {
|
|
5
|
+
readonly kind: "update";
|
|
6
|
+
readonly type: RecordTypeToken<T>;
|
|
7
|
+
readonly id: string | number;
|
|
8
|
+
/** Only the fields that change. A present `null` clears; an `undefined` value is ignored. */
|
|
9
|
+
readonly patch: Partial<T>;
|
|
10
|
+
readonly at?: undefined;
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: "update";
|
|
13
|
+
readonly type: RecordTypeToken<T>;
|
|
14
|
+
readonly id: string | number;
|
|
15
|
+
/**
|
|
16
|
+
* A field holding a keyed map (`data_points`, `attributes`): `patch` then names KEYS OF
|
|
17
|
+
* THAT MAP, merged over whatever it holds. Use it whenever a write changes one entry of a
|
|
18
|
+
* map — patching the whole field instead would pin every other entry at its value as of
|
|
19
|
+
* the click, and undoing one of two concurrent writes would undo both.
|
|
20
|
+
*/
|
|
21
|
+
readonly at: Extract<keyof T, string>;
|
|
22
|
+
readonly patch: Readonly<Record<string, unknown>>;
|
|
23
|
+
} | {
|
|
24
|
+
readonly kind: "create";
|
|
25
|
+
readonly type: RecordTypeToken<T>;
|
|
26
|
+
/** The record as the reader will see it until the server answers. Its `id` is assigned. */
|
|
27
|
+
readonly fields: Partial<T>;
|
|
28
|
+
/**
|
|
29
|
+
* Which live list reads show the pending row. Default: every list of this record type —
|
|
30
|
+
* the library cannot evaluate a server-side filter, so narrow it when a list is filtered.
|
|
31
|
+
*/
|
|
32
|
+
readonly lists?: (slot: SlotDescriptor) => boolean;
|
|
33
|
+
/** Above the fetched page (default) or below it. */
|
|
34
|
+
readonly position?: "start" | "end";
|
|
35
|
+
} | {
|
|
36
|
+
readonly kind: "delete";
|
|
37
|
+
readonly type: RecordTypeToken<T>;
|
|
38
|
+
readonly id: string | number;
|
|
39
|
+
};
|
|
40
|
+
export interface OptimisticWriteOptions<A = unknown> {
|
|
41
|
+
/**
|
|
42
|
+
* The field-set tag the server's answer satisfies, as `applyMutationResult` takes it. A
|
|
43
|
+
* create's answer ALSO satisfies the tag of every list it was shown in, or the real row would
|
|
44
|
+
* be held but not rendered the moment it replaced the pending one.
|
|
45
|
+
*/
|
|
46
|
+
readonly tag?: string;
|
|
47
|
+
/** The tenant the request is issued under. Captured now, never when the answer lands. */
|
|
48
|
+
readonly tenant?: string | number;
|
|
49
|
+
/**
|
|
50
|
+
* Pick the record out of the request's result, when the result is an envelope rather than the
|
|
51
|
+
* record itself. Default: the result, if it carries `__type` and `id`.
|
|
52
|
+
*/
|
|
53
|
+
readonly answer?: (result: A) => unknown;
|
|
54
|
+
/**
|
|
55
|
+
* The request stores its own answer — an API layer that writes every response into the store,
|
|
56
|
+
* or a caller that normalizes the answer before storing it. Commit then only retires the belief:
|
|
57
|
+
* nothing is written, so the stored answer is exactly what shows.
|
|
58
|
+
*/
|
|
59
|
+
readonly answerStored?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** An applied write whose request the caller runs itself — `beginOptimisticWrite`'s handle. */
|
|
62
|
+
export interface OptimisticHandle {
|
|
63
|
+
/** The record's id — for a create, the temporary one the pending row carries. */
|
|
64
|
+
readonly id: string | number;
|
|
65
|
+
/** `Type:id`, as the store keys it. */
|
|
66
|
+
readonly key: string;
|
|
67
|
+
/** The request succeeded. `answer` is the server's record, if it sent one. Idempotent. */
|
|
68
|
+
commit(answer?: unknown): void;
|
|
69
|
+
/** The request failed: put back exactly what was there before. Idempotent. */
|
|
70
|
+
rollback(): void;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Apply an optimistic write and run its request.
|
|
74
|
+
*
|
|
75
|
+
* Applies synchronously — before `request` is even called — so the reader sees the change in
|
|
76
|
+
* the same tick as their click, hidden page or not. On success the server's answer replaces the
|
|
77
|
+
* belief and the request's own result is returned untouched. On failure the belief is removed
|
|
78
|
+
* and the error is rethrown for the caller's refusal handling.
|
|
79
|
+
*/
|
|
80
|
+
export declare function optimisticWrite<T, A>(spec: OptimisticWriteSpec<T>, request: () => Promise<A>, options?: OptimisticWriteOptions<A>): Promise<A>;
|
|
81
|
+
/**
|
|
82
|
+
* The same write, for a caller that owns its request lifecycle — apply now, and call `commit`
|
|
83
|
+
* or `rollback` on the handle when the request settles.
|
|
84
|
+
*/
|
|
85
|
+
export declare function beginOptimisticWrite<T>(spec: OptimisticWriteSpec<T>, options?: Pick<OptimisticWriteOptions, "tag" | "tenant" | "answerStored">): OptimisticHandle;
|
|
86
|
+
/** Test seam. Nothing in the library calls this. */
|
|
87
|
+
export declare function resetOptimisticWrites(): void;
|
|
@@ -46,6 +46,36 @@ export declare function slotPendingKeys(id: string): readonly string[];
|
|
|
46
46
|
* AND ONLY WHERE IT BELONGS — see `belongsTo`.
|
|
47
47
|
*/
|
|
48
48
|
export declare function appendCreated(event: RealtimeEvent): void;
|
|
49
|
+
/** What a create's `lists` predicate is shown about each live slot. */
|
|
50
|
+
export interface SlotDescriptor {
|
|
51
|
+
/** The list read's own `key`. */
|
|
52
|
+
readonly id: string;
|
|
53
|
+
readonly type: string;
|
|
54
|
+
readonly tag: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Show an optimistic create in every slot that should list it, before the server has answered.
|
|
58
|
+
*
|
|
59
|
+
* AN OPTIMISTIC CREATE IS NOT A `created` EVENT, and it does not go into `pending`. `pending` is
|
|
60
|
+
* the server telling a list "this entered your set, position unknown" and a screen may render it
|
|
61
|
+
* as a "1 new" affordance; a create is the reader's OWN action, shown inline as a row the moment
|
|
62
|
+
* it is made. It sits beside the fetched page — above it by default, where a new row is looked
|
|
63
|
+
* for — in its own list, so a fetch landing while the create is in flight rewrites the page and
|
|
64
|
+
* leaves the pending row standing.
|
|
65
|
+
*
|
|
66
|
+
* WHICH SLOTS is the caller's to say when it can: the library cannot evaluate a server-side
|
|
67
|
+
* filter, so the default is every live slot of the record's type, and `lists` narrows it.
|
|
68
|
+
*
|
|
69
|
+
* @returns the slots the key joined — their tags are what the server's answer must satisfy for
|
|
70
|
+
* the real row to render in them.
|
|
71
|
+
*/
|
|
72
|
+
export declare function addOptimisticMember(key: string, type: string, lists: ((slot: SlotDescriptor) => boolean) | undefined, position: "start" | "end"): SlotDescriptor[];
|
|
73
|
+
/**
|
|
74
|
+
* An optimistic create has settled. With `realKey` (the server answered), the real row takes
|
|
75
|
+
* the pending row's place — in the fetched page, so it stays put until the server next orders
|
|
76
|
+
* the list. With `null` (the create failed), the pending row simply goes.
|
|
77
|
+
*/
|
|
78
|
+
export declare function settleOptimisticMember(tempKey: string, realKey: string | null): void;
|
|
49
79
|
export declare function subscribeToSlot(id: string, listener: () => void): () => void;
|
|
50
80
|
/** The keys a slot holds right now — the roots a targeted refetch matches on. */
|
|
51
81
|
export declare function slotKeys(id: string): readonly string[];
|
|
@@ -25,6 +25,95 @@ export declare function write(payload: RecordPayload, options: WriteOptions): Wr
|
|
|
25
25
|
* in one call coalesces into a single notification.
|
|
26
26
|
*/
|
|
27
27
|
export declare function writeAll(payloads: readonly RecordPayload[], options: WriteOptions): WriteAllResult;
|
|
28
|
+
/**
|
|
29
|
+
* One optimistic write, held ON TOP of a record's base rather than merged into it.
|
|
30
|
+
*
|
|
31
|
+
* ==== WHY A LAYER AND NOT A WRITE ====
|
|
32
|
+
*
|
|
33
|
+
* The first optimistic writes built on this store merged the belief into the base with a
|
|
34
|
+
* client-invented `__timestamp`, and each one then had to work around what that did:
|
|
35
|
+
*
|
|
36
|
+
* - the rollback had to be stamped NEWER than the belief, or the out-of-order guard dropped
|
|
37
|
+
* it — and a same-millisecond apply-then-revert is not newer, so a local monotonic clock
|
|
38
|
+
* was needed to get it through;
|
|
39
|
+
* - once a client timestamp was stored, the SERVER's own later answer could lose to it, since
|
|
40
|
+
* a browser clock and a database column are not the same clock;
|
|
41
|
+
* - the rollback snapshot came from a read, which strips the envelope, so `__type` had to be
|
|
42
|
+
* patched back in before the store would take it.
|
|
43
|
+
*
|
|
44
|
+
* A layer has none of those problems by construction. The base — the only thing the ordering
|
|
45
|
+
* guard adjudicates — is never touched by the belief, so the belief can never outrank a server
|
|
46
|
+
* answer and removing it is a removal, not a write that has to win anything.
|
|
47
|
+
*
|
|
48
|
+
* ==== ORDER ====
|
|
49
|
+
*
|
|
50
|
+
* Layers apply oldest first, so the newest belief about a field is the one a reader sees — a
|
|
51
|
+
* set, clear, set, clear on one field shows the last clear the moment it is made, whatever
|
|
52
|
+
* order the four answers come back in. Removing a layer from the middle leaves every later one
|
|
53
|
+
* standing: undoing write 2 of 4 does not undo 3 and 4.
|
|
54
|
+
*/
|
|
55
|
+
export interface OptimisticLayer {
|
|
56
|
+
/** Global and strictly increasing — also the issue order of the writes that made them. */
|
|
57
|
+
readonly id: number;
|
|
58
|
+
/** Data fields, stored as given — never normalized, so a patch cannot mint records. */
|
|
59
|
+
fields: Record<string, unknown>;
|
|
60
|
+
/**
|
|
61
|
+
* When set, `fields` are KEYS OF THE MAP held in this one field (a record's `data_points`,
|
|
62
|
+
* say), merged over whatever that map holds beneath — never a replacement of the whole field.
|
|
63
|
+
* Two beliefs about two different keys of one map then stay independent: undoing one leaves the
|
|
64
|
+
* other standing, and a read that rewrites the map underneath still shows through every key no
|
|
65
|
+
* belief is about.
|
|
66
|
+
*/
|
|
67
|
+
readonly at: string | undefined;
|
|
68
|
+
/** An ISO time when this layer is a delete. */
|
|
69
|
+
readonly tombstone: string | undefined;
|
|
70
|
+
/** True when this layer IS the record — an optimistic create with no server row yet. */
|
|
71
|
+
readonly created: boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Put an optimistic layer on top of `Type:id`, creating a base-less record for a create.
|
|
75
|
+
*
|
|
76
|
+
* Notification is QUEUED here, like every other store change; the optimistic-write primitive
|
|
77
|
+
* flushes synchronously afterwards, because a user-initiated write must never wait on an
|
|
78
|
+
* animation frame (a hidden page never gets one).
|
|
79
|
+
*
|
|
80
|
+
* @returns the layer id, for `removeOptimisticLayer` / `trimOptimisticLayers`
|
|
81
|
+
*/
|
|
82
|
+
export declare function pushOptimisticLayer(type: string, id: string | number, layer: {
|
|
83
|
+
fields?: Record<string, unknown>;
|
|
84
|
+
at?: string;
|
|
85
|
+
tombstone?: string;
|
|
86
|
+
created?: boolean;
|
|
87
|
+
}): number;
|
|
88
|
+
/**
|
|
89
|
+
* Take one layer off. A record that only ever existed as an optimistic create disappears with
|
|
90
|
+
* its last layer — it reads `unknown` again, exactly as before the create.
|
|
91
|
+
*/
|
|
92
|
+
export declare function removeOptimisticLayer(key: string, layerId: number): void;
|
|
93
|
+
/**
|
|
94
|
+
* A later write's answer has landed: every OLDER layer's belief about the same fields is
|
|
95
|
+
* superseded, whether or not that older request has answered yet. A layer left with nothing to
|
|
96
|
+
* say is dropped. Without this, a set whose answer is still out would keep overlaying a field
|
|
97
|
+
* the server has already confirmed was cleared after it.
|
|
98
|
+
*
|
|
99
|
+
* `at` scopes the trim to beliefs about keys of that one map field — the same granularity the
|
|
100
|
+
* committing write had.
|
|
101
|
+
*/
|
|
102
|
+
export declare function trimOptimisticLayers(key: string, beforeLayerId: number, fields: readonly string[], at?: string): void;
|
|
103
|
+
/**
|
|
104
|
+
* Whether the BASE is tombstoned — what a list slot prunes on. An optimistic delete hides the
|
|
105
|
+
* row from every read but must not cost the slot its key, or a rollback would bring the record
|
|
106
|
+
* back with no list left holding it.
|
|
107
|
+
*/
|
|
108
|
+
export declare function isBaseTombstoned(key: string): boolean;
|
|
109
|
+
/**
|
|
110
|
+
* One field of the BASE, as stored (normalized: a nested record is a `{__ref}`). What a keyed
|
|
111
|
+
* belief is folded into when its write succeeds without an answer to replace it — writing it back
|
|
112
|
+
* through `write()` round-trips the refs untouched.
|
|
113
|
+
*/
|
|
114
|
+
export declare function readBaseField(key: string, field: string): unknown;
|
|
115
|
+
/** Does `key` carry any optimistic layer right now? */
|
|
116
|
+
export declare function hasOptimisticLayers(key: string): boolean;
|
|
28
117
|
/**
|
|
29
118
|
* The `unknown` state, as ONE module-level frozen constant.
|
|
30
119
|
*
|
|
@@ -54,7 +143,9 @@ export declare function peekRecord(type: string, id: string | number, needs: str
|
|
|
54
143
|
*
|
|
55
144
|
* Exported so a caller that must observe the effect of a write synchronously —
|
|
56
145
|
* a test, or a measurement taken immediately after a write — can flush rather
|
|
57
|
-
* than wait for a frame.
|
|
146
|
+
* than wait for a frame. The optimistic write (`reads/optimistic.ts`) flushes
|
|
147
|
+
* after every step: a reader's own action is one change, not a burst, and it
|
|
148
|
+
* must reach the screen in the same tick even on a page that gets no frames.
|
|
58
149
|
*/
|
|
59
150
|
export declare function flushRecordNotifications(): void;
|
|
60
151
|
export declare function subscribeToKey(key: string, listener: () => void): () => void;
|