@danxbot/ui 7.4.3 → 7.5.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.
@@ -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,68 @@
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
+ } | {
11
+ readonly kind: "create";
12
+ readonly type: RecordTypeToken<T>;
13
+ /** The record as the reader will see it until the server answers. Its `id` is assigned. */
14
+ readonly fields: Partial<T>;
15
+ /**
16
+ * Which live list reads show the pending row. Default: every list of this record type —
17
+ * the library cannot evaluate a server-side filter, so narrow it when a list is filtered.
18
+ */
19
+ readonly lists?: (slot: SlotDescriptor) => boolean;
20
+ /** Above the fetched page (default) or below it. */
21
+ readonly position?: "start" | "end";
22
+ } | {
23
+ readonly kind: "delete";
24
+ readonly type: RecordTypeToken<T>;
25
+ readonly id: string | number;
26
+ };
27
+ export interface OptimisticWriteOptions<A = unknown> {
28
+ /**
29
+ * The field-set tag the server's answer satisfies, as `applyMutationResult` takes it. A
30
+ * create's answer ALSO satisfies the tag of every list it was shown in, or the real row would
31
+ * be held but not rendered the moment it replaced the pending one.
32
+ */
33
+ readonly tag?: string;
34
+ /** The tenant the request is issued under. Captured now, never when the answer lands. */
35
+ readonly tenant?: string | number;
36
+ /**
37
+ * Pick the record out of the request's result, when the result is an envelope rather than the
38
+ * record itself. Default: the result, if it carries `__type` and `id`.
39
+ */
40
+ readonly answer?: (result: A) => unknown;
41
+ }
42
+ /** An applied write whose request the caller runs itself — `beginOptimisticWrite`'s handle. */
43
+ export interface OptimisticHandle {
44
+ /** The record's id — for a create, the temporary one the pending row carries. */
45
+ readonly id: string | number;
46
+ /** `Type:id`, as the store keys it. */
47
+ readonly key: string;
48
+ /** The request succeeded. `answer` is the server's record, if it sent one. Idempotent. */
49
+ commit(answer?: unknown): void;
50
+ /** The request failed: put back exactly what was there before. Idempotent. */
51
+ rollback(): void;
52
+ }
53
+ /**
54
+ * Apply an optimistic write and run its request.
55
+ *
56
+ * Applies synchronously — before `request` is even called — so the reader sees the change in
57
+ * the same tick as their click, hidden page or not. On success the server's answer replaces the
58
+ * belief and the request's own result is returned untouched. On failure the belief is removed
59
+ * and the error is rethrown for the caller's refusal handling.
60
+ */
61
+ export declare function optimisticWrite<T, A>(spec: OptimisticWriteSpec<T>, request: () => Promise<A>, options?: OptimisticWriteOptions<A>): Promise<A>;
62
+ /**
63
+ * The same write, for a caller that owns its request lifecycle — apply now, and call `commit`
64
+ * or `rollback` on the handle when the request settles.
65
+ */
66
+ export declare function beginOptimisticWrite<T>(spec: OptimisticWriteSpec<T>, options?: Pick<OptimisticWriteOptions, "tag" | "tenant">): OptimisticHandle;
67
+ /** Test seam. Nothing in the library calls this. */
68
+ 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,77 @@ 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
+ /** An ISO time when this layer is a delete. */
61
+ readonly tombstone: string | undefined;
62
+ /** True when this layer IS the record — an optimistic create with no server row yet. */
63
+ readonly created: boolean;
64
+ }
65
+ /**
66
+ * Put an optimistic layer on top of `Type:id`, creating a base-less record for a create.
67
+ *
68
+ * Notification is QUEUED here, like every other store change; the optimistic-write primitive
69
+ * flushes synchronously afterwards, because a user-initiated write must never wait on an
70
+ * animation frame (a hidden page never gets one).
71
+ *
72
+ * @returns the layer id, for `removeOptimisticLayer` / `trimOptimisticLayers`
73
+ */
74
+ export declare function pushOptimisticLayer(type: string, id: string | number, layer: {
75
+ fields?: Record<string, unknown>;
76
+ tombstone?: string;
77
+ created?: boolean;
78
+ }): number;
79
+ /**
80
+ * Take one layer off. A record that only ever existed as an optimistic create disappears with
81
+ * its last layer — it reads `unknown` again, exactly as before the create.
82
+ */
83
+ export declare function removeOptimisticLayer(key: string, layerId: number): void;
84
+ /**
85
+ * A later write's answer has landed: every OLDER layer's belief about the same fields is
86
+ * superseded, whether or not that older request has answered yet. A layer left with nothing to
87
+ * say is dropped. Without this, a set whose answer is still out would keep overlaying a field
88
+ * the server has already confirmed was cleared after it.
89
+ */
90
+ export declare function trimOptimisticLayers(key: string, beforeLayerId: number, fields: readonly string[]): void;
91
+ /**
92
+ * Whether the BASE is tombstoned — what a list slot prunes on. An optimistic delete hides the
93
+ * row from every read but must not cost the slot its key, or a rollback would bring the record
94
+ * back with no list left holding it.
95
+ */
96
+ export declare function isBaseTombstoned(key: string): boolean;
97
+ /** Does `key` carry any optimistic layer right now? */
98
+ export declare function hasOptimisticLayers(key: string): boolean;
28
99
  /**
29
100
  * The `unknown` state, as ONE module-level frozen constant.
30
101
  *
@@ -54,7 +125,9 @@ export declare function peekRecord(type: string, id: string | number, needs: str
54
125
  *
55
126
  * Exported so a caller that must observe the effect of a write synchronously —
56
127
  * a test, or a measurement taken immediately after a write — can flush rather
57
- * than wait for a frame.
128
+ * than wait for a frame. The optimistic write (`reads/optimistic.ts`) flushes
129
+ * after every step: a reader's own action is one change, not a burst, and it
130
+ * must reach the screen in the same tick even on a page that gets no frames.
58
131
  */
59
132
  export declare function flushRecordNotifications(): void;
60
133
  export declare function subscribeToKey(key: string, listener: () => void): () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danxbot/ui",
3
- "version": "7.4.3",
3
+ "version": "7.5.0",
4
4
  "type": "module",
5
5
  "description": "Danxbot — a domain-agnostic React design system with motion as a first-class primitive.",
6
6
  "license": "MIT",