@braidlabs/angular-data 0.3.0 → 0.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.
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import * as i0 from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { EnvironmentProviders, InjectionToken, Signal } from "@angular/core";
|
|
3
|
+
import { OptimisticOverlay, QueuedEntry, RecordDriver } from "@braidlabs/data";
|
|
5
4
|
/**
|
|
6
5
|
* Entity identity.
|
|
7
6
|
*
|
|
@@ -10,15 +9,15 @@ import { QueuedEntry, OptimisticOverlay, RecordDriver } from '@braidlabs/data';
|
|
|
10
9
|
* updating one leaves the other stale. Deduplicating them requires knowing that
|
|
11
10
|
* both payloads describe the same thing — which requires a declared identity.
|
|
12
11
|
*/
|
|
13
|
-
interface EntityType<T> {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
export interface EntityType<T> {
|
|
13
|
+
/** Stable name, used for store partitioning and tag generation. */
|
|
14
|
+
readonly name: string;
|
|
15
|
+
/** Extracts the primary key. Must be stable and collision-free per type. */
|
|
16
|
+
readonly key: (value: T) => string;
|
|
18
17
|
}
|
|
19
|
-
interface EntityDefinition<T> {
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
export interface EntityDefinition<T> {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly key: (value: T) => string;
|
|
22
21
|
}
|
|
23
22
|
/**
|
|
24
23
|
* Declares an entity type.
|
|
@@ -30,101 +29,99 @@ interface EntityDefinition<T> {
|
|
|
30
29
|
* Declare each type once and export it — identity is only useful if every
|
|
31
30
|
* query and mutation agrees on it.
|
|
32
31
|
*/
|
|
33
|
-
declare function entity<T>(definition: EntityDefinition<T>): EntityType<T>;
|
|
32
|
+
export declare function entity<T>(definition: EntityDefinition<T>): EntityType<T>;
|
|
34
33
|
/** Tag helpers, so invalidation reads the same way everywhere. */
|
|
35
|
-
declare const tag: {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
export declare const tag: {
|
|
35
|
+
/** A single record: `bulletin#42`. */
|
|
36
|
+
readonly entity: <T>(type: EntityType<T>, id: string) => string;
|
|
37
|
+
/** Every record of a type: `bulletin#*`. */
|
|
38
|
+
readonly all: <T>(type: EntityType<T>) => string;
|
|
39
|
+
/** An arbitrary named collection or view. */
|
|
40
|
+
readonly collection: (name: string) => string;
|
|
42
41
|
};
|
|
43
|
-
|
|
44
42
|
/**
|
|
45
43
|
* A batch of writes that can be rolled back wholesale.
|
|
46
44
|
*
|
|
47
45
|
* Optimistic updates need this: the write happens before the server has agreed,
|
|
48
46
|
* so a failure must restore precisely what was there — not "roughly re-fetch".
|
|
49
47
|
*/
|
|
50
|
-
interface StoreTransaction {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
export interface StoreTransaction {
|
|
49
|
+
upsert<T>(type: EntityType<T>, values: T | readonly T[]): void;
|
|
50
|
+
patch<T>(type: EntityType<T>, id: string, partial: Partial<T>): void;
|
|
51
|
+
remove<T>(type: EntityType<T>, id: string): void;
|
|
54
52
|
}
|
|
55
|
-
declare class EntityStore {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
53
|
+
export declare class EntityStore {
|
|
54
|
+
private readonly state;
|
|
55
|
+
private readonly pending;
|
|
56
|
+
/**
|
|
57
|
+
* Memoized per `type#id`. Without this, calling `select()` inside a template
|
|
58
|
+
* would mint a fresh computed on every change-detection pass — a subtle and
|
|
59
|
+
* expensive footgun.
|
|
60
|
+
*/
|
|
61
|
+
private readonly selectCache;
|
|
62
|
+
private readonly allCache;
|
|
63
|
+
/** A single record as this page shows it: confirmed, with any pending write applied. */
|
|
64
|
+
select<T>(type: EntityType<T>, id: string): Signal<T | undefined>;
|
|
65
|
+
/**
|
|
66
|
+
* Every record of a type, in insertion order, with pending writes applied.
|
|
67
|
+
*
|
|
68
|
+
* A pending record nothing has confirmed yet still appears, and a pending removal does not: a
|
|
69
|
+
* list that omits what the user just created, or keeps what they just deleted, is the same bug
|
|
70
|
+
* seen from either end.
|
|
71
|
+
*/
|
|
72
|
+
selectAll<T>(type: EntityType<T>): Signal<readonly T[]>;
|
|
73
|
+
/**
|
|
74
|
+
* A filtered view. Not memoized by predicate — call it once and keep the
|
|
75
|
+
* result, as you would any other `computed`.
|
|
76
|
+
*/
|
|
77
|
+
query<T>(type: EntityType<T>, predicate: (value: T) => boolean): Signal<readonly T[]>;
|
|
78
|
+
/**
|
|
79
|
+
* Reads a record without subscribing. For use inside effects and handlers.
|
|
80
|
+
*
|
|
81
|
+
* The same value `select` would give, pending writes included — a read that disagrees with the
|
|
82
|
+
* subscribed read depending on which one you happened to call is a trap, not an optimization.
|
|
83
|
+
* {@link peekConfirmed} is the one that skips the overlay.
|
|
84
|
+
*/
|
|
85
|
+
peek<T>(type: EntityType<T>, id: string): T | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* The record as the server last reported it, ignoring anything queued.
|
|
88
|
+
*
|
|
89
|
+
* What a conflict is measured against: "the server disagreed with you" is a claim about the
|
|
90
|
+
* prediction, so comparing against a value that already includes the prediction would answer a
|
|
91
|
+
* different question.
|
|
92
|
+
*/
|
|
93
|
+
peekConfirmed<T>(type: EntityType<T>, id: string): T | undefined;
|
|
94
|
+
upsert<T>(type: EntityType<T>, values: T | readonly T[]): void;
|
|
95
|
+
patch<T>(type: EntityType<T>, id: string, partial: Partial<T>): void;
|
|
96
|
+
remove<T>(type: EntityType<T>, id: string): void;
|
|
97
|
+
/** Empties a type, or the whole store. Mainly for sign-out and tests. */
|
|
98
|
+
clear<T>(type?: EntityType<T>): void;
|
|
99
|
+
/**
|
|
100
|
+
* Applies writes that can be undone as a unit.
|
|
101
|
+
*
|
|
102
|
+
* ```ts
|
|
103
|
+
* const tx = store.transaction();
|
|
104
|
+
* tx.apply((t) => t.patch(Bulletin, id, { status: 'published' }));
|
|
105
|
+
* // …server says no…
|
|
106
|
+
* tx.rollback();
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
transaction(): {
|
|
110
|
+
apply(fn: (tx: StoreTransaction) => void): void;
|
|
111
|
+
rollback(): void;
|
|
112
|
+
committed: boolean;
|
|
113
|
+
};
|
|
114
|
+
/** `confirmed ⊕ pending` for one record. The single place the two halves meet. */
|
|
115
|
+
private view;
|
|
116
|
+
private toWrites;
|
|
117
|
+
/**
|
|
118
|
+
* Single write path, so every mutation produces exactly one signal
|
|
119
|
+
* notification regardless of how many records it touched.
|
|
120
|
+
*/
|
|
121
|
+
private applyWrites;
|
|
122
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<EntityStore, never>;
|
|
123
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
126
124
|
}
|
|
127
|
-
|
|
128
125
|
/**
|
|
129
126
|
* The optimistic overlay: what the queue predicts, applied over the confirmed graph.
|
|
130
127
|
*
|
|
@@ -144,56 +141,56 @@ declare class EntityStore {
|
|
|
144
141
|
* from the queue, and another app's unsent edits show up here because they are in the same store.
|
|
145
142
|
*/
|
|
146
143
|
/** One record's worth of prediction, in the entity store's terms. */
|
|
147
|
-
interface EntityOverlay {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
144
|
+
export interface EntityOverlay {
|
|
145
|
+
readonly typeName: string;
|
|
146
|
+
readonly id: string;
|
|
147
|
+
/** Applied over the confirmed record. Empty for a removal. */
|
|
148
|
+
readonly patch: Record<string, unknown>;
|
|
149
|
+
/** True when the write deletes the record — absence a patch cannot express. */
|
|
150
|
+
readonly removed?: boolean;
|
|
154
151
|
}
|
|
155
|
-
declare class PendingWrites {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
152
|
+
export declare class PendingWrites {
|
|
153
|
+
private readonly groups;
|
|
154
|
+
/** Every pending overlay on the page, in the order the writes were made. */
|
|
155
|
+
readonly overlays: Signal<readonly EntityOverlay[]>;
|
|
156
|
+
/** Whether anything on screen is showing work the server has not confirmed. */
|
|
157
|
+
readonly hasPending: Signal<boolean>;
|
|
158
|
+
/**
|
|
159
|
+
* Records a write's prediction.
|
|
160
|
+
*
|
|
161
|
+
* Applied synchronously, before the entry reaches storage: a UI that waits for IndexedDB to show
|
|
162
|
+
* what the user just typed is a UI that flickers. Storage remains the truth — `syncFromQueue`
|
|
163
|
+
* replaces this the moment the queue is next read.
|
|
164
|
+
*/
|
|
165
|
+
add(id: string, overlays: readonly EntityOverlay[], options: {
|
|
166
|
+
durable: boolean;
|
|
167
|
+
}): void;
|
|
168
|
+
/** Drops a group: a confirmation, or a rollback. There is no difference at this level. */
|
|
169
|
+
settle(id: string): void;
|
|
170
|
+
/** Re-keys a group once its entry id is known, so the queue's copy replaces the same group. */
|
|
171
|
+
rename(from: string, to: string): void;
|
|
172
|
+
/**
|
|
173
|
+
* Rebuilds every durable group from the queue.
|
|
174
|
+
*
|
|
175
|
+
* This is the derivation. Called on every outbox refresh — including the one after rehydrating
|
|
176
|
+
* from storage, which is how a reload gets the overlay back without anything in memory having
|
|
177
|
+
* survived, and how one app comes to show another's unsent edits.
|
|
178
|
+
*/
|
|
179
|
+
syncFromQueue(entries: readonly QueuedEntry[]): void;
|
|
180
|
+
/** Sign-out, and tests. */
|
|
181
|
+
clear(): void;
|
|
182
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<PendingWrites, never>;
|
|
183
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
187
184
|
}
|
|
188
185
|
/** `bulletin#42` — the same key `tag.entity` produces, so one write's tag and overlay agree. */
|
|
189
|
-
declare function overlayKey(typeName: string, id: string): string;
|
|
190
|
-
declare function toOutboxOverlay(overlay: EntityOverlay): OptimisticOverlay;
|
|
186
|
+
export declare function overlayKey(typeName: string, id: string): string;
|
|
187
|
+
export declare function toOutboxOverlay(overlay: EntityOverlay): OptimisticOverlay;
|
|
191
188
|
/**
|
|
192
189
|
* Splits at the *first* `#`, because an id may contain one and a type name may not.
|
|
193
190
|
*/
|
|
194
|
-
declare function fromOutboxOverlay(overlay: OptimisticOverlay): EntityOverlay;
|
|
191
|
+
export declare function fromOutboxOverlay(overlay: OptimisticOverlay): EntityOverlay;
|
|
195
192
|
/** Applies the overlays for one record, in order. `undefined` means pending removal. */
|
|
196
|
-
declare function applyOverlays<T>(confirmed: T | undefined, overlays: readonly EntityOverlay[]): T | undefined;
|
|
193
|
+
export declare function applyOverlays<T>(confirmed: T | undefined, overlays: readonly EntityOverlay[]): T | undefined;
|
|
197
194
|
/**
|
|
198
195
|
* Runs an `optimistic(tx, input)` callback against a transaction that **records** rather than
|
|
199
196
|
* writes, turning the callback into overlay data.
|
|
@@ -204,19 +201,18 @@ declare function applyOverlays<T>(confirmed: T | undefined, overlays: readonly E
|
|
|
204
201
|
* was expressed. Recording it makes the same description survive a reload, which the closure could
|
|
205
202
|
* never do.
|
|
206
203
|
*/
|
|
207
|
-
interface RecordedWrites {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
204
|
+
export interface RecordedWrites {
|
|
205
|
+
readonly overlays: EntityOverlay[];
|
|
206
|
+
/**
|
|
207
|
+
* The entity types the callback named, by name.
|
|
208
|
+
*
|
|
209
|
+
* Kept beside the overlays rather than inside them because a type carries a `key` function, and a
|
|
210
|
+
* function cannot be persisted — the overlays go to storage, this map stays in the closure that is
|
|
211
|
+
* waiting on the server.
|
|
212
|
+
*/
|
|
213
|
+
readonly types: Map<string, EntityType<unknown>>;
|
|
217
214
|
}
|
|
218
|
-
declare function recordOverlays(apply: (tx: StoreTransaction) => void): RecordedWrites;
|
|
219
|
-
|
|
215
|
+
export declare function recordOverlays(apply: (tx: StoreTransaction) => void): RecordedWrites;
|
|
220
216
|
/**
|
|
221
217
|
* Tag-based invalidation.
|
|
222
218
|
*
|
|
@@ -229,102 +225,100 @@ declare function recordOverlays(apply: (tx: StoreTransaction) => void): Recorded
|
|
|
229
225
|
* matches every `bulletin#…` subscriber. A subscriber may also register the
|
|
230
226
|
* wildcard itself, in which case any invalidation of that type reaches it.
|
|
231
227
|
*/
|
|
232
|
-
declare class CacheRegistry {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
228
|
+
export declare class CacheRegistry {
|
|
229
|
+
private readonly subscribers;
|
|
230
|
+
/**
|
|
231
|
+
* Registers interest in a set of tags. Returns a disposer.
|
|
232
|
+
*
|
|
233
|
+
* Tags are supplied as a getter and re-read on each invalidation, so a query
|
|
234
|
+
* whose tags depend on signals (a route param, a filter) stays correct
|
|
235
|
+
* without having to re-subscribe.
|
|
236
|
+
*/
|
|
237
|
+
subscribe(getTags: () => readonly string[], notify: () => void): () => void;
|
|
238
|
+
/**
|
|
239
|
+
* Marks tags stale, notifying every matching subscriber.
|
|
240
|
+
*
|
|
241
|
+
* Notification is synchronous but failure-isolated: one subscriber throwing
|
|
242
|
+
* must not prevent the others from refreshing.
|
|
243
|
+
*/
|
|
244
|
+
invalidate(...tags: readonly string[]): void;
|
|
245
|
+
/** Subscriber count. Diagnostics and tests only. */
|
|
246
|
+
get size(): number;
|
|
247
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<CacheRegistry, never>;
|
|
248
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
253
249
|
}
|
|
254
250
|
/**
|
|
255
251
|
* `bulletin#*` matches `bulletin#42` in either position, so a mutation can
|
|
256
252
|
* invalidate a whole type without enumerating ids, and a list query can
|
|
257
253
|
* subscribe to a whole type without knowing which ids it will receive.
|
|
258
254
|
*/
|
|
259
|
-
declare function tagsMatch(a: string, b: string): boolean;
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
readonly buildId?: string;
|
|
255
|
+
export declare function tagsMatch(a: string, b: string): boolean;
|
|
256
|
+
export interface DataOptions {
|
|
257
|
+
/**
|
|
258
|
+
* Where the outbox is stored. Shared across every app on the origin.
|
|
259
|
+
*
|
|
260
|
+
* A record-oriented driver rather than a key/value store, and that is the correctness fix: the
|
|
261
|
+
* queue used to live under a single key, so appending meant read-modify-write and two apps
|
|
262
|
+
* interleaving lost each other's entries.
|
|
263
|
+
*/
|
|
264
|
+
readonly driver: RecordDriver;
|
|
265
|
+
/**
|
|
266
|
+
* Which application this is, stamped onto every entry it queues.
|
|
267
|
+
*
|
|
268
|
+
* The store is shared, so ownership is what keeps one app from acting on another's queued work.
|
|
269
|
+
* Only the owner replays an entry; everyone else leaves it alone.
|
|
270
|
+
*/
|
|
271
|
+
readonly owner: string;
|
|
272
|
+
/** Attempts before a queued entry is abandoned. Default 5. */
|
|
273
|
+
readonly maxOutboxAttempts: number;
|
|
274
|
+
/**
|
|
275
|
+
* Reports outbox trouble: an abandoned entry, a mutation this build no longer has, a failed write.
|
|
276
|
+
*
|
|
277
|
+
* Silent by default, but wiring it to telemetry is how you find out that users are losing queued
|
|
278
|
+
* work — the failure is otherwise invisible, because the user already navigated away believing it
|
|
279
|
+
* saved.
|
|
280
|
+
*/
|
|
281
|
+
readonly onOutboxError?: (message: string, detail?: unknown) => void;
|
|
282
|
+
/** Flush the outbox when the browser reports it is back online. Default true. */
|
|
283
|
+
readonly flushOnReconnect: boolean;
|
|
284
|
+
readonly buildId?: string;
|
|
290
285
|
}
|
|
291
|
-
declare const DATA_OPTIONS: InjectionToken<DataOptions>;
|
|
292
|
-
interface DataOptionsInput {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
286
|
+
export declare const DATA_OPTIONS: InjectionToken<DataOptions>;
|
|
287
|
+
export interface DataOptionsInput {
|
|
288
|
+
/**
|
|
289
|
+
* Identifies this application in the shared outbox. **Required when persisting.**
|
|
290
|
+
*
|
|
291
|
+
* Not defaulted, deliberately. A default would be the same string in every app on the page, which
|
|
292
|
+
* is precisely the collision ownership exists to prevent — and it would fail silently, in
|
|
293
|
+
* production, on someone's unsent work.
|
|
294
|
+
*/
|
|
295
|
+
readonly owner?: string;
|
|
296
|
+
/** Supply storage directly. Overrides `persistOutbox`. */
|
|
297
|
+
readonly driver?: RecordDriver;
|
|
298
|
+
readonly maxOutboxAttempts?: number;
|
|
299
|
+
readonly onOutboxError?: DataOptions['onOutboxError'];
|
|
300
|
+
readonly flushOnReconnect?: boolean;
|
|
301
|
+
/**
|
|
302
|
+
* Persist the outbox in IndexedDB, so queued work survives a reload.
|
|
303
|
+
*
|
|
304
|
+
* Off by default: queued work is then lost on reload, which is only appropriate when every
|
|
305
|
+
* mutation is fire-and-forget.
|
|
306
|
+
*/
|
|
307
|
+
readonly persistOutbox?: boolean;
|
|
308
|
+
/** Stamped onto persisted envelopes for attribution. */
|
|
309
|
+
readonly buildId?: string;
|
|
310
|
+
/** Database name when persisting. Defaults to `skew-data`. */
|
|
311
|
+
readonly database?: string;
|
|
312
|
+
/**
|
|
313
|
+
* Extra collections to create alongside the outbox.
|
|
314
|
+
*
|
|
315
|
+
* Declared up front because IndexedDB can only create object stores during a version upgrade —
|
|
316
|
+
* a collection used but not declared fails at the first read, not at configuration time.
|
|
317
|
+
*/
|
|
318
|
+
readonly collections?: readonly string[];
|
|
324
319
|
}
|
|
325
|
-
declare const OUTBOX_COLLECTION = "outbox";
|
|
326
|
-
declare function resolveDataOptions(input?: DataOptionsInput): DataOptions;
|
|
327
|
-
|
|
320
|
+
export declare const OUTBOX_COLLECTION = "outbox";
|
|
321
|
+
export declare function resolveDataOptions(input?: DataOptionsInput): DataOptions;
|
|
328
322
|
/**
|
|
329
323
|
* The durable mutation outbox, as an Angular service over `@braidlabs/data`.
|
|
330
324
|
*
|
|
@@ -343,101 +337,100 @@ declare function resolveDataOptions(input?: DataOptionsInput): DataOptions;
|
|
|
343
337
|
* happened to be wired.
|
|
344
338
|
*/
|
|
345
339
|
/** A queued entry as this app sees it. */
|
|
346
|
-
type OutboxEntry = QueuedEntry;
|
|
347
|
-
type OutboxRunner = (input: unknown, entry: OutboxEntry) => Promise<unknown>;
|
|
348
|
-
interface FlushResult {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
340
|
+
export type OutboxEntry = QueuedEntry;
|
|
341
|
+
export type OutboxRunner = (input: unknown, entry: OutboxEntry) => Promise<unknown>;
|
|
342
|
+
export interface FlushResult {
|
|
343
|
+
sent: number;
|
|
344
|
+
failed: number;
|
|
345
|
+
remaining: number;
|
|
346
|
+
/**
|
|
347
|
+
* True when this call did no work because someone else was already flushing — another tab, or a
|
|
348
|
+
* re-entrant call.
|
|
349
|
+
*
|
|
350
|
+
* Distinct from `sent === 0`, which also happens when the flush *ran* and everything failed.
|
|
351
|
+
* Conflating them makes a failing server look like a busy tab, which sends whoever is debugging
|
|
352
|
+
* in exactly the wrong direction.
|
|
353
|
+
*/
|
|
354
|
+
skipped: boolean;
|
|
361
355
|
}
|
|
362
|
-
declare class OutboxService {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
356
|
+
export declare class OutboxService {
|
|
357
|
+
private readonly options;
|
|
358
|
+
private readonly pending;
|
|
359
|
+
private readonly runners;
|
|
360
|
+
private readonly mineSignal;
|
|
361
|
+
private readonly foreignSignal;
|
|
362
|
+
private readonly flushingSignal;
|
|
363
|
+
/** This app's queued work, oldest first — what it can actually replay. */
|
|
364
|
+
readonly entries: Signal<readonly OutboxEntry[]>;
|
|
365
|
+
/**
|
|
366
|
+
* Work queued by another app on this page. Waiting, not stuck: it replays when its owner is
|
|
367
|
+
* mounted. Surfaced so an app can render "someone has unsent changes" honestly rather than
|
|
368
|
+
* reporting only what it can see of the queue.
|
|
369
|
+
*/
|
|
370
|
+
readonly foreignEntries: Signal<readonly OutboxEntry[]>;
|
|
371
|
+
/**
|
|
372
|
+
* Unsent work across the whole page, not just this app's.
|
|
373
|
+
*
|
|
374
|
+
* "Are there unsent changes?" is a page-wide question, and each app answering only for itself is
|
|
375
|
+
* how the indicator was wrong before. Use {@link entries} when you need what *this* app owns.
|
|
376
|
+
*/
|
|
377
|
+
readonly pendingCount: Signal<number>;
|
|
378
|
+
readonly hasPendingWork: Signal<boolean>;
|
|
379
|
+
readonly isFlushing: Signal<boolean>;
|
|
380
|
+
private outbox;
|
|
381
|
+
private loaded;
|
|
382
|
+
private get queue();
|
|
383
|
+
/**
|
|
384
|
+
* Registers how a mutation id replays. Called by `mutation()` at creation, which is why outbox
|
|
385
|
+
* mutations must be constructed during app start-up rather than lazily inside a click handler —
|
|
386
|
+
* otherwise a queued entry has nothing to replay into after a reload.
|
|
387
|
+
*/
|
|
388
|
+
register(mutationId: string, runner: OutboxRunner): void;
|
|
389
|
+
/** Rehydrates from storage. Safe to call repeatedly. */
|
|
390
|
+
load(): Promise<void>;
|
|
391
|
+
enqueue(entry: {
|
|
392
|
+
mutationId: string;
|
|
393
|
+
input: unknown;
|
|
394
|
+
schemaVersion?: number;
|
|
395
|
+
/** What readers should show for the records this entry changes, until it is confirmed. */
|
|
396
|
+
optimistic?: OptimisticOverlay[];
|
|
397
|
+
}): Promise<OutboxEntry>;
|
|
398
|
+
/**
|
|
399
|
+
* Drains this app's queue in order.
|
|
400
|
+
*
|
|
401
|
+
* The rules — sequential, stop at the first failure, one drain per owner across every tab, give up
|
|
402
|
+
* loudly — live in `@braidlabs/data`'s `drainOutbox`, because this service used to carry a second
|
|
403
|
+
* copy of them and the two had already drifted apart on what to do with an entry nobody can
|
|
404
|
+
* replay. What stays here is the Angular half: signals, and re-reading the shared store into them.
|
|
405
|
+
*
|
|
406
|
+
* Only this app's entries are touched. Another app's queued work is left exactly where it is.
|
|
407
|
+
*/
|
|
408
|
+
flush(): Promise<FlushResult>;
|
|
409
|
+
/**
|
|
410
|
+
* Removes one entry, and with it the overlay derived from it.
|
|
411
|
+
*
|
|
412
|
+
* What a confirmation does, and equally what a rollback does — at this level they are the same
|
|
413
|
+
* operation, which is the point of deriving the optimistic view rather than keeping an undo log.
|
|
414
|
+
*/
|
|
415
|
+
remove(id: string): Promise<void>;
|
|
416
|
+
/**
|
|
417
|
+
* Discards this app's queued work. Sign-out, or an explicit "abandon my changes".
|
|
418
|
+
*
|
|
419
|
+
* Another app's entries survive: discarding work this app never queued and cannot replay would be
|
|
420
|
+
* deciding on someone else's behalf.
|
|
421
|
+
*/
|
|
422
|
+
clear(): Promise<void>;
|
|
423
|
+
/**
|
|
424
|
+
* Re-reads the shared store into signals. The store is the truth; signals are a view of it.
|
|
425
|
+
*
|
|
426
|
+
* The optimistic overlay is rebuilt here too, from the same read. That is what makes the overlay
|
|
427
|
+
* derived rather than remembered: a reload, another app's write, and a rolled-back entry all reach
|
|
428
|
+
* the screen through this one path, because all three are just a different queue.
|
|
429
|
+
*/
|
|
430
|
+
private refresh;
|
|
431
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<OutboxService, never>;
|
|
432
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
439
433
|
}
|
|
440
|
-
|
|
441
434
|
/**
|
|
442
435
|
* A read that normalizes into the shared store.
|
|
443
436
|
*
|
|
@@ -446,27 +439,27 @@ declare class OutboxService {
|
|
|
446
439
|
* So this is a parallel primitive rather than an extension — duplication we
|
|
447
440
|
* would happily delete if the framework grew the hook.
|
|
448
441
|
*/
|
|
449
|
-
type QueryStatus = 'idle' | 'loading' | 'success' | 'error';
|
|
450
|
-
interface QueryConfig<T> {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
442
|
+
export type QueryStatus = 'idle' | 'loading' | 'success' | 'error';
|
|
443
|
+
export interface QueryConfig<T> {
|
|
444
|
+
/** Fetches the data. Anything promise-returning. */
|
|
445
|
+
readonly loader: () => Promise<T>;
|
|
446
|
+
/**
|
|
447
|
+
* Entity types present in the response. Every matching record is written to
|
|
448
|
+
* the store, so other views of the same entity update automatically.
|
|
449
|
+
*/
|
|
450
|
+
readonly normalize?: EntityType<any> | ReadonlyArray<EntityType<any>>;
|
|
451
|
+
/** Tags this query depends on; invalidating any of them re-runs it. */
|
|
452
|
+
readonly tags?: () => readonly string[];
|
|
453
|
+
/** Run immediately on creation. Default true. */
|
|
454
|
+
readonly immediate?: boolean;
|
|
462
455
|
}
|
|
463
|
-
interface QueryRef<T> {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
456
|
+
export interface QueryRef<T> {
|
|
457
|
+
readonly value: Signal<T | undefined>;
|
|
458
|
+
readonly status: Signal<QueryStatus>;
|
|
459
|
+
readonly error: Signal<unknown>;
|
|
460
|
+
readonly isLoading: Signal<boolean>;
|
|
461
|
+
/** Re-runs the loader, bypassing any tag bookkeeping. */
|
|
462
|
+
reload(): Promise<void>;
|
|
470
463
|
}
|
|
471
464
|
/**
|
|
472
465
|
* Creates a query. Must be called in an injection context.
|
|
@@ -482,7 +475,7 @@ interface QueryRef<T> {
|
|
|
482
475
|
* readonly rows = this.store.selectAll(Bulletin);
|
|
483
476
|
* ```
|
|
484
477
|
*/
|
|
485
|
-
declare function query<T>(config: QueryConfig<T>): QueryRef<T>;
|
|
478
|
+
export declare function query<T>(config: QueryConfig<T>): QueryRef<T>;
|
|
486
479
|
/**
|
|
487
480
|
* Walks a payload and writes every recognised entity into the store.
|
|
488
481
|
*
|
|
@@ -490,8 +483,7 @@ declare function query<T>(config: QueryConfig<T>): QueryRef<T>;
|
|
|
490
483
|
* such as `{ items: [...] }`. Anything unrecognised is left alone rather than
|
|
491
484
|
* guessed at — a wrong write into the shared graph is worse than no write.
|
|
492
485
|
*/
|
|
493
|
-
declare function normalizeInto(store: EntityStore, types: EntityType<any> | ReadonlyArray<EntityType<any>>, payload: unknown): void;
|
|
494
|
-
|
|
486
|
+
export declare function normalizeInto(store: EntityStore, types: EntityType<any> | ReadonlyArray<EntityType<any>>, payload: unknown): void;
|
|
495
487
|
/**
|
|
496
488
|
* A write, with the three things every application ends up reimplementing:
|
|
497
489
|
* optimistic application, rollback on failure, and durability.
|
|
@@ -508,7 +500,7 @@ declare function normalizeInto(store: EntityStore, types: EntityType<any> | Read
|
|
|
508
500
|
* reload — so a user who queues a change offline and refreshes sees the *old*
|
|
509
501
|
* value while their edit sits in storage waiting to send.
|
|
510
502
|
*/
|
|
511
|
-
type MutationStatus = 'idle' | 'pending' | 'success' | 'error';
|
|
503
|
+
export type MutationStatus = 'idle' | 'pending' | 'success' | 'error';
|
|
512
504
|
/**
|
|
513
505
|
* The server accepted the write and stored something else.
|
|
514
506
|
*
|
|
@@ -516,18 +508,18 @@ type MutationStatus = 'idle' | 'pending' | 'success' | 'error';
|
|
|
516
508
|
* not know about — which is exactly why it is so easy to ship a UI that swaps
|
|
517
509
|
* the value under whoever just typed it.
|
|
518
510
|
*/
|
|
519
|
-
interface MutationConflict {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
511
|
+
export interface MutationConflict {
|
|
512
|
+
/** The record as the screen showed it: confirmed, with this write's prediction applied. */
|
|
513
|
+
expected: unknown;
|
|
514
|
+
/** The record the server reported. */
|
|
515
|
+
actual: unknown;
|
|
516
|
+
/** The predicted fields the server disagreed about. */
|
|
517
|
+
paths: readonly string[];
|
|
518
|
+
/** Which record this is about. */
|
|
519
|
+
entity: {
|
|
520
|
+
typeName: string;
|
|
521
|
+
id: string;
|
|
522
|
+
};
|
|
531
523
|
}
|
|
532
524
|
/**
|
|
533
525
|
* What to do when the server disagrees.
|
|
@@ -536,59 +528,59 @@ interface MutationConflict {
|
|
|
536
528
|
* server hold your value without another mutation — so the only question is whether the user is
|
|
537
529
|
* told, and the silent answer is a support ticket about someone's money.
|
|
538
530
|
*/
|
|
539
|
-
type ConflictPolicy = 'raise' | 'accept' | ((conflict: MutationConflict) => unknown);
|
|
540
|
-
interface MutationConfig<TInput, TResult> {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
531
|
+
export type ConflictPolicy = 'raise' | 'accept' | ((conflict: MutationConflict) => unknown);
|
|
532
|
+
export interface MutationConfig<TInput, TResult> {
|
|
533
|
+
/**
|
|
534
|
+
* Stable identifier. Required when `durability: 'outbox'`, because a queued
|
|
535
|
+
* entry outlives the closure and must find its operation again by name after
|
|
536
|
+
* a reload.
|
|
537
|
+
*/
|
|
538
|
+
readonly id?: string;
|
|
539
|
+
readonly operation: (input: TInput) => Promise<TResult>;
|
|
540
|
+
/**
|
|
541
|
+
* Describes the change locally before the server has agreed.
|
|
542
|
+
*
|
|
543
|
+
* Still a callback taking a transaction, and still written exactly as it was —
|
|
544
|
+
* but the transaction now **records** rather than writes. `tx.patch(Bulletin,
|
|
545
|
+
* id, { status: 'published' })` was always a description; recording it is what
|
|
546
|
+
* lets the same description survive a reload, which a closure never could.
|
|
547
|
+
*/
|
|
548
|
+
readonly optimistic?: (tx: StoreTransaction, input: TInput) => void;
|
|
549
|
+
/**
|
|
550
|
+
* Normalizes the server's response back into the store — the confirmed half.
|
|
551
|
+
*
|
|
552
|
+
* Receives the value chosen by `onConflict` when a resolver returned one, so a
|
|
553
|
+
* team that reconciles a disagreement writes the reconciled record rather than
|
|
554
|
+
* having to re-apply it afterwards.
|
|
555
|
+
*/
|
|
556
|
+
readonly onSuccess?: (store: EntityStore, result: TResult, input: TInput) => void;
|
|
557
|
+
/** Tags to mark stale once the write lands. */
|
|
558
|
+
readonly invalidates?: (input: TInput, result?: TResult) => readonly string[];
|
|
559
|
+
/**
|
|
560
|
+
* `'outbox'` persists the mutation so it survives a reload and replays when
|
|
561
|
+
* connectivity returns. `'memory'` (default) fails fast.
|
|
562
|
+
*
|
|
563
|
+
* The overlay is derived from the queue either way. The difference is what
|
|
564
|
+
* rebuilds it: a persisted entry can be read back after a reload, and an
|
|
565
|
+
* in-memory one correctly dies with the page that made it.
|
|
566
|
+
*/
|
|
567
|
+
readonly durability?: 'memory' | 'outbox';
|
|
568
|
+
/** Payload contract version, carried on queued entries. */
|
|
569
|
+
readonly schemaVersion?: number;
|
|
570
|
+
/** Defaults to `'raise'`. */
|
|
571
|
+
readonly onConflict?: ConflictPolicy;
|
|
580
572
|
}
|
|
581
|
-
interface MutationRef<TInput, TResult> {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
573
|
+
export interface MutationRef<TInput, TResult> {
|
|
574
|
+
mutate(input: TInput): Promise<TResult>;
|
|
575
|
+
readonly status: Signal<MutationStatus>;
|
|
576
|
+
readonly error: Signal<unknown>;
|
|
577
|
+
readonly isPending: Signal<boolean>;
|
|
578
|
+
/** True while this mutation's prediction is on screen and unconfirmed. */
|
|
579
|
+
readonly hasPendingWrite: Signal<boolean>;
|
|
580
|
+
/** The last disagreement, under the `'raise'` policy. Cleared by the next attempt. */
|
|
581
|
+
readonly conflict: Signal<MutationConflict | null>;
|
|
582
|
+
/** Dismisses a raised conflict, once the user has seen it. */
|
|
583
|
+
acknowledgeConflict(): void;
|
|
592
584
|
}
|
|
593
585
|
/**
|
|
594
586
|
* Creates a mutation. Must be called in an injection context.
|
|
@@ -604,8 +596,7 @@ interface MutationRef<TInput, TResult> {
|
|
|
604
596
|
* });
|
|
605
597
|
* ```
|
|
606
598
|
*/
|
|
607
|
-
declare function mutation<TInput, TResult>(config: MutationConfig<TInput, TResult>): MutationRef<TInput, TResult>;
|
|
608
|
-
|
|
599
|
+
export declare function mutation<TInput, TResult>(config: MutationConfig<TInput, TResult>): MutationRef<TInput, TResult>;
|
|
609
600
|
/**
|
|
610
601
|
* Enables the data layer.
|
|
611
602
|
*
|
|
@@ -618,7 +609,4 @@ declare function mutation<TInput, TResult>(config: MutationConfig<TInput, TResul
|
|
|
618
609
|
* });
|
|
619
610
|
* ```
|
|
620
611
|
*/
|
|
621
|
-
declare function provideSkewData(input?: DataOptionsInput): EnvironmentProviders;
|
|
622
|
-
|
|
623
|
-
export { CacheRegistry, DATA_OPTIONS, EntityStore, OUTBOX_COLLECTION, OutboxService, PendingWrites, applyOverlays, entity, fromOutboxOverlay, mutation, normalizeInto, overlayKey, provideSkewData, query, recordOverlays, resolveDataOptions, tag, tagsMatch, toOutboxOverlay };
|
|
624
|
-
export type { ConflictPolicy, DataOptions, DataOptionsInput, EntityDefinition, EntityOverlay, EntityType, FlushResult, MutationConfig, MutationConflict, MutationRef, MutationStatus, OutboxEntry, OutboxRunner, QueryConfig, QueryRef, QueryStatus, RecordedWrites, StoreTransaction };
|
|
612
|
+
export declare function provideSkewData(input?: DataOptionsInput): EnvironmentProviders;
|