@braidlabs/angular-data 0.7.1 → 0.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.
@@ -409,7 +409,8 @@ function resolveDataOptions(input = {}) {
409
409
  maxOutboxAttempts: input.maxOutboxAttempts ?? 5,
410
410
  ...input.onOutboxError === void 0 ? {} : { onOutboxError: input.onOutboxError },
411
411
  flushOnReconnect: input.flushOnReconnect ?? true,
412
- ...input.buildId === void 0 ? {} : { buildId: input.buildId }
412
+ ...input.buildId === void 0 ? {} : { buildId: input.buildId },
413
+ ...input.partition === void 0 ? {} : { partition: input.partition }
413
414
  };
414
415
  }
415
416
  var OutboxService = class OutboxService {
@@ -431,7 +432,8 @@ var OutboxService = class OutboxService {
431
432
  driver: this.options.driver,
432
433
  owner: this.options.owner,
433
434
  collection: OUTBOX_COLLECTION,
434
- ...this.options.buildId === void 0 ? {} : { buildId: this.options.buildId }
435
+ ...this.options.buildId === void 0 ? {} : { buildId: this.options.buildId },
436
+ ...this.options.partition === void 0 ? {} : { partition: this.options.partition }
435
437
  });
436
438
  return this.outbox;
437
439
  }
@@ -440,8 +442,8 @@ var OutboxService = class OutboxService {
440
442
  }
441
443
  async load() {
442
444
  if (this.loaded) return;
443
- this.loaded = true;
444
445
  await this.refresh();
446
+ this.loaded = true;
445
447
  }
446
448
  async enqueue(entry) {
447
449
  await this.load();
@@ -698,8 +700,9 @@ function provideSkewData(input = {}) {
698
700
  }, provideEnvironmentInitializer(() => {
699
701
  const outbox = inject(OutboxService);
700
702
  if (!isPlatformBrowser(inject(PLATFORM_ID))) return;
701
- outbox.load().then(() => outbox.flush());
702
- if (options.flushOnReconnect) inject(DOCUMENT).defaultView?.addEventListener("online", () => void outbox.flush());
703
+ const report = (error) => options.onOutboxError?.(`[skew/data] the outbox could not be read: ${String(error)}`, error);
704
+ outbox.load().then(() => outbox.flush()).catch(report);
705
+ if (options.flushOnReconnect) inject(DOCUMENT).defaultView?.addEventListener("online", () => void outbox.flush().catch(report));
703
706
  })]);
704
707
  }
705
708
  export { CacheRegistry, DATA_OPTIONS, EntityStore, OUTBOX_COLLECTION, OutboxService, PendingWrites, applyOverlays, entity, fromOutboxOverlay, mutation, normalizeInto, overlayKey, provideSkewData, query, recordOverlays, resolveDataOptions, tag, tagsMatch, toOutboxOverlay };
@@ -1 +1 @@
1
- {"version":3,"file":"braidlabs-angular-data.mjs","names":[],"sources":["../../../../../libs/angular/data/src/lib/entity.ts","../../../../../libs/angular/data/src/lib/overlay.ts","../../../../../libs/angular/data/src/lib/store.ts","../../../../../libs/angular/data/src/lib/cache-registry.ts","../../../../../libs/angular/data/src/lib/config.ts","../../../../../libs/angular/data/src/lib/outbox.ts","../../../../../libs/angular/data/src/lib/query.ts","../../../../../libs/angular/data/src/lib/mutation.ts","../../../../../libs/angular/data/src/lib/provide.ts"],"sourcesContent":["/**\n * Entity identity.\n *\n * The gap this closes: `httpResource()` and friends are *per-call* caches. Two\n * requests that return the same record produce two independent copies, and\n * updating one leaves the other stale. Deduplicating them requires knowing that\n * both payloads describe the same thing — which requires a declared identity.\n */\n\nexport interface EntityType<T> {\n /** Stable name, used for store partitioning and tag generation. */\n readonly name: string;\n /** Extracts the primary key. Must be stable and collision-free per type. */\n readonly key: (value: T) => string;\n}\n\nexport interface EntityDefinition<T> {\n readonly name: string;\n readonly key: (value: T) => string;\n}\n\n/**\n * Declares an entity type.\n *\n * ```ts\n * export const Bulletin = entity<Bulletin>({ name: 'bulletin', key: (b) => b.id });\n * ```\n *\n * Declare each type once and export it — identity is only useful if every\n * query and mutation agrees on it.\n */\nexport function entity<T>(definition: EntityDefinition<T>): EntityType<T> {\n if (!definition.name) {\n throw new TypeError('[skew/data] entity() requires a name');\n }\n if (typeof definition.key !== 'function') {\n throw new TypeError(`[skew/data] entity \"${definition.name}\" requires a key function`);\n }\n return { name: definition.name, key: definition.key };\n}\n\n/** Tag helpers, so invalidation reads the same way everywhere. */\nexport const tag = {\n /** A single record: `bulletin#42`. */\n entity<T>(type: EntityType<T>, id: string): string {\n return `${type.name}#${id}`;\n },\n /** Every record of a type: `bulletin#*`. */\n all<T>(type: EntityType<T>): string {\n return `${type.name}#*`;\n },\n /** An arbitrary named collection or view. */\n collection(name: string): string {\n return name;\n },\n} as const;\n","import { Injectable, Signal, computed, signal } from '@angular/core';\nimport type { OptimisticOverlay, QueuedEntry } from '@braidlabs/data';\nimport type { EntityType } from './entity';\nimport type { StoreTransaction } from './store';\n\n/**\n * The optimistic overlay: what the queue predicts, applied over the confirmed graph.\n *\n * view(record) = confirmed(record) ⊕ pending(record)\n *\n * The important word is *derived*. This used to be an apply-then-undo: a write patched the store,\n * an undo log remembered the previous values, and a failure restored them. Two things went wrong\n * with that, and both are the kind of bug nobody reports as one:\n *\n * 1. **The overlay and the queue could disagree.** A queued mutation lives in storage; its\n * optimistic effect lived in memory. Reload the page and the work was still queued while the\n * screen showed the old value — the user's edit apparently lost, then reappearing later when the\n * queue drained.\n * 2. **Only the app that wrote could see it.** The queue is shared per origin; an undo log is not.\n *\n * Deriving fixes both at once. Rollback becomes deleting an entry, a reload rebuilds the overlay\n * from the queue, and another app's unsent edits show up here because they are in the same store.\n */\n\n/** One record's worth of prediction, in the entity store's terms. */\nexport interface EntityOverlay {\n readonly typeName: string;\n readonly id: string;\n /** Applied over the confirmed record. Empty for a removal. */\n readonly patch: Record<string, unknown>;\n /** True when the write deletes the record — absence a patch cannot express. */\n readonly removed?: boolean;\n}\n\n/**\n * Overlays that belong to one write, so settling or rolling back is one operation.\n *\n * `durable` marks the groups the queue owns: those are replaced wholesale on every re-read of the\n * queue, which is what makes storage the source of truth rather than this signal. Groups that are\n * not durable belong to `durability: 'memory'` mutations — nothing persisted them, so nothing can\n * rebuild them, and they correctly die with the page.\n */\ninterface OverlayGroup {\n readonly id: string;\n readonly durable: boolean;\n readonly overlays: readonly EntityOverlay[];\n}\n\n@Injectable({ providedIn: 'root' })\nexport class PendingWrites {\n private readonly groups = signal<readonly OverlayGroup[]>([]);\n\n /** Every pending overlay on the page, in the order the writes were made. */\n readonly overlays: Signal<readonly EntityOverlay[]> = computed(() =>\n this.groups().flatMap((group) => group.overlays),\n );\n\n /** Whether anything on screen is showing work the server has not confirmed. */\n readonly hasPending = computed(() => this.groups().length > 0);\n\n /**\n * Records a write's prediction.\n *\n * Applied synchronously, before the entry reaches storage: a UI that waits for IndexedDB to show\n * what the user just typed is a UI that flickers. Storage remains the truth — `syncFromQueue`\n * replaces this the moment the queue is next read.\n */\n add(id: string, overlays: readonly EntityOverlay[], options: { durable: boolean }): void {\n if (overlays.length === 0) return;\n this.groups.update((groups) => [\n ...groups.filter((group) => group.id !== id),\n { id, durable: options.durable, overlays },\n ]);\n }\n\n /** Drops a group: a confirmation, or a rollback. There is no difference at this level. */\n settle(id: string): void {\n this.groups.update((groups) => groups.filter((group) => group.id !== id));\n }\n\n /** Re-keys a group once its entry id is known, so the queue's copy replaces the same group. */\n rename(from: string, to: string): void {\n this.groups.update((groups) =>\n groups.map((group) => (group.id === from ? { ...group, id: to, durable: true } : group)),\n );\n }\n\n /**\n * Rebuilds every durable group from the queue.\n *\n * This is the derivation. Called on every outbox refresh — including the one after rehydrating\n * from storage, which is how a reload gets the overlay back without anything in memory having\n * survived, and how one app comes to show another's unsent edits.\n */\n syncFromQueue(entries: readonly QueuedEntry[]): void {\n const fromQueue = entries\n .filter((entry) => (entry.optimistic ?? []).length > 0)\n .map((entry) => ({\n id: entry.id,\n durable: true,\n overlays: (entry.optimistic ?? []).map(fromOutboxOverlay),\n }));\n\n this.groups.update((groups) => [...groups.filter((group) => !group.durable), ...fromQueue]);\n }\n\n /** Sign-out, and tests. */\n clear(): void {\n this.groups.set([]);\n }\n}\n\n/** `bulletin#42` — the same key `tag.entity` produces, so one write's tag and overlay agree. */\nexport function overlayKey(typeName: string, id: string): string {\n return `${typeName}#${id}`;\n}\n\nexport function toOutboxOverlay(overlay: EntityOverlay): OptimisticOverlay {\n return {\n key: overlayKey(overlay.typeName, overlay.id),\n patch: overlay.patch,\n ...(overlay.removed ? { removed: true } : {}),\n };\n}\n\n/**\n * Splits at the *first* `#`, because an id may contain one and a type name may not.\n */\nexport function fromOutboxOverlay(overlay: OptimisticOverlay): EntityOverlay {\n const at = overlay.key.indexOf('#');\n return {\n typeName: at === -1 ? overlay.key : overlay.key.slice(0, at),\n id: at === -1 ? '' : overlay.key.slice(at + 1),\n patch: overlay.patch,\n ...(overlay.removed ? { removed: true } : {}),\n };\n}\n\n/** Applies the overlays for one record, in order. `undefined` means pending removal. */\nexport function applyOverlays<T>(\n confirmed: T | undefined,\n overlays: readonly EntityOverlay[],\n): T | undefined {\n let value = confirmed;\n for (const overlay of overlays) {\n value = overlay.removed ? undefined : ({ ...(value ?? {}), ...overlay.patch } as T);\n }\n return value;\n}\n\n/**\n * Runs an `optimistic(tx, input)` callback against a transaction that **records** rather than\n * writes, turning the callback into overlay data.\n *\n * This is what lets the existing config surface stay exactly as it is while the mechanism\n * underneath changes: `tx.patch(Bulletin, id, { status: 'published' })` describes a patch perfectly\n * well, and a closure was never the part that mattered — it just happened to be how the description\n * was expressed. Recording it makes the same description survive a reload, which the closure could\n * never do.\n */\nexport interface RecordedWrites {\n readonly overlays: EntityOverlay[];\n /**\n * The entity types the callback named, by name.\n *\n * Kept beside the overlays rather than inside them because a type carries a `key` function, and a\n * function cannot be persisted — the overlays go to storage, this map stays in the closure that is\n * waiting on the server.\n */\n readonly types: Map<string, EntityType<unknown>>;\n}\n\nexport function recordOverlays(apply: (tx: StoreTransaction) => void): RecordedWrites {\n const recorded: EntityOverlay[] = [];\n const types = new Map<string, EntityType<unknown>>();\n\n const remember = <T>(type: EntityType<T>) => types.set(type.name, type as EntityType<unknown>);\n\n const tx: StoreTransaction = {\n upsert<T>(type: EntityType<T>, values: T | readonly T[]) {\n remember(type);\n const list = Array.isArray(values) ? (values as readonly T[]) : [values as T];\n for (const value of list) {\n recorded.push({\n typeName: type.name,\n id: type.key(value),\n patch: value as unknown as Record<string, unknown>,\n });\n }\n },\n patch<T>(type: EntityType<T>, id: string, partial: Partial<T>) {\n remember(type);\n recorded.push({ typeName: type.name, id, patch: partial as Record<string, unknown> });\n },\n remove<T>(type: EntityType<T>, id: string) {\n remember(type);\n recorded.push({ typeName: type.name, id, patch: {}, removed: true });\n },\n };\n\n apply(tx);\n return { overlays: recorded, types };\n}\n","import { Injectable, Signal, computed, inject, signal } from '@angular/core';\nimport type { EntityType } from './entity';\nimport { PendingWrites, applyOverlays, type EntityOverlay } from './overlay';\n\n/**\n * The normalized entity store.\n *\n * Components must read from *here*, not from the response object a query\n * resolved. This is the part teams get wrong: if a component holds the parsed\n * response, writing an updated record into the store changes nothing it can\n * observe, and normalization buys you exactly nothing.\n *\n * ✗ bulletins = this.list.value(); // a private copy that drifts\n * ✓ bulletins = store.selectAll(Bulletin); // a view of the shared graph\n *\n * **Reads are the view, not the confirmed graph.** Every read here returns\n * `confirmed ⊕ pending`: what the server has agreed to, with the queue's\n * predictions applied on top. Writes go only to the confirmed half — an\n * optimistic change is a queued entry, never a write here — which is why a\n * failed mutation needs no undo log and a reload shows the user their unsent\n * edit instead of the value they replaced. See `overlay.ts`.\n */\n\ntype EntityTable = ReadonlyMap<string, unknown>;\ntype StoreState = ReadonlyMap<string, EntityTable>;\n\n/** Records applied within a transaction, so they can be undone. */\ninterface UndoEntry {\n readonly typeName: string;\n readonly id: string;\n /** `undefined` means \"did not exist\", so undo removes it. */\n readonly previous: unknown | undefined;\n}\n\n/**\n * A batch of writes that can be rolled back wholesale.\n *\n * Optimistic updates need this: the write happens before the server has agreed,\n * so a failure must restore precisely what was there — not \"roughly re-fetch\".\n */\nexport interface StoreTransaction {\n upsert<T>(type: EntityType<T>, values: T | readonly T[]): void;\n patch<T>(type: EntityType<T>, id: string, partial: Partial<T>): void;\n remove<T>(type: EntityType<T>, id: string): void;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class EntityStore {\n private readonly state = signal<StoreState>(new Map());\n private readonly pending = inject(PendingWrites);\n\n /**\n * Memoized per `type#id`. Without this, calling `select()` inside a template\n * would mint a fresh computed on every change-detection pass — a subtle and\n * expensive footgun.\n */\n private readonly selectCache = new Map<string, Signal<unknown>>();\n private readonly allCache = new Map<string, Signal<readonly unknown[]>>();\n\n /** A single record as this page shows it: confirmed, with any pending write applied. */\n select<T>(type: EntityType<T>, id: string): Signal<T | undefined> {\n const cacheKey = `${type.name}#${id}`;\n const existing = this.selectCache.get(cacheKey);\n if (existing) return existing as Signal<T | undefined>;\n\n const derived = computed(() => this.view<T>(type, id));\n this.selectCache.set(cacheKey, derived);\n return derived;\n }\n\n /**\n * Every record of a type, in insertion order, with pending writes applied.\n *\n * A pending record nothing has confirmed yet still appears, and a pending removal does not: a\n * list that omits what the user just created, or keeps what they just deleted, is the same bug\n * seen from either end.\n */\n selectAll<T>(type: EntityType<T>): Signal<readonly T[]> {\n const existing = this.allCache.get(type.name);\n if (existing) return existing as Signal<readonly T[]>;\n\n const derived = computed(() => {\n const table = this.state().get(type.name);\n const overlaid = this.pending.overlays().filter((overlay) => overlay.typeName === type.name);\n const ids = new Set([...(table?.keys() ?? []), ...overlaid.map((overlay) => overlay.id)]);\n\n const rows: T[] = [];\n for (const id of ids) {\n const value = this.view<T>(type, id);\n if (value !== undefined) rows.push(value);\n }\n return rows;\n });\n this.allCache.set(type.name, derived);\n return derived;\n }\n\n /**\n * A filtered view. Not memoized by predicate — call it once and keep the\n * result, as you would any other `computed`.\n */\n query<T>(type: EntityType<T>, predicate: (value: T) => boolean): Signal<readonly T[]> {\n const all = this.selectAll(type);\n return computed(() => all().filter(predicate));\n }\n\n /**\n * Reads a record without subscribing. For use inside effects and handlers.\n *\n * The same value `select` would give, pending writes included — a read that disagrees with the\n * subscribed read depending on which one you happened to call is a trap, not an optimization.\n * {@link peekConfirmed} is the one that skips the overlay.\n */\n peek<T>(type: EntityType<T>, id: string): T | undefined {\n return this.view<T>(type, id);\n }\n\n /**\n * The record as the server last reported it, ignoring anything queued.\n *\n * What a conflict is measured against: \"the server disagreed with you\" is a claim about the\n * prediction, so comparing against a value that already includes the prediction would answer a\n * different question.\n */\n peekConfirmed<T>(type: EntityType<T>, id: string): T | undefined {\n return this.state().get(type.name)?.get(id) as T | undefined;\n }\n\n upsert<T>(type: EntityType<T>, values: T | readonly T[]): void {\n this.applyWrites(this.toWrites(type, values));\n }\n\n patch<T>(type: EntityType<T>, id: string, partial: Partial<T>): void {\n const current = this.peek(type, id);\n if (current === undefined) return;\n this.applyWrites([{ typeName: type.name, id, value: { ...current, ...partial } }]);\n }\n\n remove<T>(type: EntityType<T>, id: string): void {\n this.applyWrites([{ typeName: type.name, id, value: undefined }]);\n }\n\n /** Empties a type, or the whole store. Mainly for sign-out and tests. */\n clear<T>(type?: EntityType<T>): void {\n if (!type) {\n this.state.set(new Map());\n return;\n }\n const next = new Map(this.state());\n next.delete(type.name);\n this.state.set(next);\n }\n\n /**\n * Applies writes that can be undone as a unit.\n *\n * ```ts\n * const tx = store.transaction();\n * tx.apply((t) => t.patch(Bulletin, id, { status: 'published' }));\n * // …server says no…\n * tx.rollback();\n * ```\n */\n transaction(): {\n apply(fn: (tx: StoreTransaction) => void): void;\n rollback(): void;\n committed: boolean;\n } {\n const undoLog: UndoEntry[] = [];\n let rolledBack = false;\n\n const record = (typeName: string, id: string): void => {\n // Only the *first* prior value matters; later writes in the same\n // transaction are intermediate states we never want to restore to.\n if (undoLog.some((entry) => entry.typeName === typeName && entry.id === id)) return;\n undoLog.push({ typeName, id, previous: this.state().get(typeName)?.get(id) });\n };\n\n const tx: StoreTransaction = {\n upsert: <T>(type: EntityType<T>, values: T | readonly T[]) => {\n const writes = this.toWrites(type, values);\n for (const write of writes) record(write.typeName, write.id);\n this.applyWrites(writes);\n },\n patch: <T>(type: EntityType<T>, id: string, partial: Partial<T>) => {\n record(type.name, id);\n const current = this.peek(type, id);\n if (current === undefined) return;\n this.applyWrites([{ typeName: type.name, id, value: { ...current, ...partial } }]);\n },\n remove: <T>(type: EntityType<T>, id: string) => {\n record(type.name, id);\n this.applyWrites([{ typeName: type.name, id, value: undefined }]);\n },\n };\n\n return {\n apply(fn) {\n fn(tx);\n },\n rollback: () => {\n if (rolledBack) return;\n rolledBack = true;\n this.applyWrites(\n // Restore in reverse so overlapping writes unwind correctly.\n [...undoLog].reverse().map((entry) => ({\n typeName: entry.typeName,\n id: entry.id,\n value: entry.previous,\n })),\n );\n },\n get committed() {\n return !rolledBack;\n },\n };\n }\n\n // --- internals ---------------------------------------------------------\n\n /** `confirmed ⊕ pending` for one record. The single place the two halves meet. */\n private view<T>(type: EntityType<T>, id: string): T | undefined {\n const confirmed = this.state().get(type.name)?.get(id) as T | undefined;\n const overlays: EntityOverlay[] = this.pending\n .overlays()\n .filter((overlay) => overlay.typeName === type.name && overlay.id === id);\n\n if (overlays.length === 0) return confirmed;\n return applyOverlays(confirmed, overlays);\n }\n\n private toWrites<T>(\n type: EntityType<T>,\n values: T | readonly T[],\n ): Array<{ typeName: string; id: string; value: unknown }> {\n const list = Array.isArray(values) ? (values as readonly T[]) : [values as T];\n return list.map((value) => ({\n typeName: type.name,\n id: type.key(value),\n value,\n }));\n }\n\n /**\n * Single write path, so every mutation produces exactly one signal\n * notification regardless of how many records it touched.\n */\n private applyWrites(writes: ReadonlyArray<{ typeName: string; id: string; value: unknown }>): void {\n if (writes.length === 0) return;\n\n const next = new Map(this.state());\n const touched = new Map<string, Map<string, unknown>>();\n\n for (const { typeName, id, value } of writes) {\n let table = touched.get(typeName);\n if (!table) {\n table = new Map(next.get(typeName) ?? []);\n touched.set(typeName, table);\n }\n if (value === undefined) table.delete(id);\n else table.set(id, value);\n }\n\n for (const [typeName, table] of touched) next.set(typeName, table);\n this.state.set(next);\n }\n}\n","import { Injectable } from '@angular/core';\n\n/**\n * Tag-based invalidation.\n *\n * Tags rather than TTLs, because a time-to-live is a guess about when data went\n * stale, whereas a mutation *knows*. `bulletin#42` and `bulletins` are claims\n * about what a query depends on; invalidating a tag re-runs exactly the queries\n * that said they cared.\n *\n * Wildcards are supported in one direction only: invalidating `bulletin#*`\n * matches every `bulletin#…` subscriber. A subscriber may also register the\n * wildcard itself, in which case any invalidation of that type reaches it.\n */\n@Injectable({ providedIn: 'root' })\nexport class CacheRegistry {\n private readonly subscribers = new Map<\n symbol,\n { readonly getTags: () => readonly string[]; readonly notify: () => void }\n >();\n\n /**\n * Registers interest in a set of tags. Returns a disposer.\n *\n * Tags are supplied as a getter and re-read on each invalidation, so a query\n * whose tags depend on signals (a route param, a filter) stays correct\n * without having to re-subscribe.\n */\n subscribe(getTags: () => readonly string[], notify: () => void): () => void {\n const token = Symbol('skew-cache-subscriber');\n this.subscribers.set(token, { getTags, notify });\n return () => void this.subscribers.delete(token);\n }\n\n /**\n * Marks tags stale, notifying every matching subscriber.\n *\n * Notification is synchronous but failure-isolated: one subscriber throwing\n * must not prevent the others from refreshing.\n */\n invalidate(...tags: readonly string[]): void {\n if (tags.length === 0) return;\n for (const subscriber of [...this.subscribers.values()]) {\n let interested = false;\n try {\n interested = subscriber.getTags().some((owned) => tags.some((t) => tagsMatch(owned, t)));\n } catch {\n // A tag getter that throws (a signal read outside its context, say)\n // must not take down invalidation for everyone else.\n interested = false;\n }\n if (!interested) continue;\n try {\n subscriber.notify();\n } catch {\n // A refresh that throws is that query's problem, not this loop's.\n }\n }\n }\n\n /** Subscriber count. Diagnostics and tests only. */\n get size(): number {\n return this.subscribers.size;\n }\n}\n\n/**\n * `bulletin#*` matches `bulletin#42` in either position, so a mutation can\n * invalidate a whole type without enumerating ids, and a list query can\n * subscribe to a whole type without knowing which ids it will receive.\n */\nexport function tagsMatch(a: string, b: string): boolean {\n if (a === b) return true;\n const wildcardOf = (tag: string) => (tag.endsWith('#*') ? tag.slice(0, -1) : null);\n\n const aPrefix = wildcardOf(a);\n if (aPrefix && b.startsWith(aPrefix)) return true;\n\n const bPrefix = wildcardOf(b);\n if (bPrefix && a.startsWith(bPrefix)) return true;\n\n return false;\n}\n","import { InjectionToken } from '@angular/core';\nimport { indexedDbRecordDriver, memoryRecordDriver, type RecordDriver } from '@braidlabs/data';\n\nexport interface DataOptions {\n /**\n * Where the outbox is stored. Shared across every app on the origin.\n *\n * A record-oriented driver rather than a key/value store, and that is the correctness fix: the\n * queue used to live under a single key, so appending meant read-modify-write and two apps\n * interleaving lost each other's entries.\n */\n readonly driver: RecordDriver;\n /**\n * Which application this is, stamped onto every entry it queues.\n *\n * The store is shared, so ownership is what keeps one app from acting on another's queued work.\n * Only the owner replays an entry; everyone else leaves it alone.\n */\n readonly owner: string;\n /** Attempts before a queued entry is abandoned. Default 5. */\n readonly maxOutboxAttempts: number;\n /**\n * Reports outbox trouble: an abandoned entry, a mutation this build no longer has, a failed write.\n *\n * Silent by default, but wiring it to telemetry is how you find out that users are losing queued\n * work — the failure is otherwise invisible, because the user already navigated away believing it\n * saved.\n */\n readonly onOutboxError?: (message: string, detail?: unknown) => void;\n /** Flush the outbox when the browser reports it is back online. Default true. */\n readonly flushOnReconnect: boolean;\n readonly buildId?: string;\n}\n\nexport const DATA_OPTIONS = new InjectionToken<DataOptions>('SKEW_DATA_OPTIONS');\n\nexport interface DataOptionsInput {\n /**\n * Identifies this application in the shared outbox. **Required when persisting.**\n *\n * Not defaulted, deliberately. A default would be the same string in every app on the page, which\n * is precisely the collision ownership exists to prevent — and it would fail silently, in\n * production, on someone's unsent work.\n */\n readonly owner?: string;\n /** Supply storage directly. Overrides `persistOutbox`. */\n readonly driver?: RecordDriver;\n readonly maxOutboxAttempts?: number;\n readonly onOutboxError?: DataOptions['onOutboxError'];\n readonly flushOnReconnect?: boolean;\n /**\n * Persist the outbox in IndexedDB, so queued work survives a reload.\n *\n * Off by default: queued work is then lost on reload, which is only appropriate when every\n * mutation is fire-and-forget.\n */\n readonly persistOutbox?: boolean;\n /** Stamped onto persisted envelopes for attribution. */\n readonly buildId?: string;\n /** Database name when persisting. Defaults to `skew-data`. */\n readonly database?: string;\n /**\n * Extra collections to create alongside the outbox.\n *\n * Declared up front because IndexedDB can only create object stores during a version upgrade —\n * a collection used but not declared fails at the first read, not at configuration time.\n */\n readonly collections?: readonly string[];\n}\n\nexport const OUTBOX_COLLECTION = 'outbox';\n\n/**\n * Whether this environment can persist at all.\n *\n * False during server rendering, where there is no IndexedDB — and no point in one, since the\n * outbox exists to survive a *client* reload. Falling back to memory keeps `persistOutbox: true` a\n * safe thing to write in a config shared by both bootstraps, which is where it naturally goes.\n */\nfunction canPersist(): boolean {\n return typeof globalThis.indexedDB !== 'undefined';\n}\n\nfunction persistentDriver(input: DataOptionsInput) {\n return indexedDbRecordDriver({\n collections: [OUTBOX_COLLECTION, ...(input.collections ?? [])],\n ...(input.database === undefined ? {} : { database: input.database }),\n });\n}\n\nexport function resolveDataOptions(input: DataOptionsInput = {}): DataOptions {\n const persisting = input.persistOutbox === true || input.driver !== undefined;\n\n if (persisting && !input.owner) {\n throw new Error(\n '[skew/data] a persisted outbox needs `owner`: a string identifying this application.\\n' +\n 'The outbox is shared across every app on the origin, and ownership is what stops one app ' +\n \"from replaying or discarding another's queued mutations. Defaulting it would put every app \" +\n 'under the same name, which is the collision it exists to prevent.',\n );\n }\n\n return {\n driver: input.driver ?? (input.persistOutbox && canPersist() ? persistentDriver(input) : memoryRecordDriver()),\n owner: input.owner ?? 'app',\n maxOutboxAttempts: input.maxOutboxAttempts ?? 5,\n ...(input.onOutboxError === undefined ? {} : { onOutboxError: input.onOutboxError }),\n flushOnReconnect: input.flushOnReconnect ?? true,\n ...(input.buildId === undefined ? {} : { buildId: input.buildId }),\n };\n}\n","import { Injectable, Signal, computed, inject, signal } from '@angular/core';\nimport { createOutbox, drainOutbox, type Outbox, type OptimisticOverlay, type QueuedEntry } from '@braidlabs/data';\nimport { DATA_OPTIONS, OUTBOX_COLLECTION } from './config';\nimport { PendingWrites } from './overlay';\n\n/**\n * The durable mutation outbox, as an Angular service over `@braidlabs/data`.\n *\n * This is the piece that cannot be built with in-flight request machinery. A mutation queued while\n * offline has to survive a page reload — and after a reload there is no pending `Promise` to retry,\n * no `HttpRequest` to intercept, and no closure left alive. Only something persisted can be\n * replayed.\n *\n * Which forces one API constraint: an outbox mutation must have a stable `id`, because the\n * *operation* is a closure and closures do not serialise. On replay we look the operation up by id.\n *\n * **The store is shared and the entries are owned.** Persistence lives in `@braidlabs/data`, one\n * record per entry, in a store every app on the origin reads. This service replays only what it\n * owns. Previously the queue was a single blob under one key, so a second app overwrote the first's\n * and then dropped its entries for having no registered runner — silently, unless `onOutboxError`\n * happened to be wired.\n */\n\n/** A queued entry as this app sees it. */\nexport type OutboxEntry = QueuedEntry;\n\nexport type OutboxRunner = (input: unknown, entry: OutboxEntry) => Promise<unknown>;\n\nexport interface FlushResult {\n sent: number;\n failed: number;\n remaining: number;\n /**\n * True when this call did no work because someone else was already flushing — another tab, or a\n * re-entrant call.\n *\n * Distinct from `sent === 0`, which also happens when the flush *ran* and everything failed.\n * Conflating them makes a failing server look like a busy tab, which sends whoever is debugging\n * in exactly the wrong direction.\n */\n skipped: boolean;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class OutboxService {\n private readonly options = inject(DATA_OPTIONS);\n private readonly pending = inject(PendingWrites);\n private readonly runners = new Map<string, OutboxRunner>();\n\n private readonly mineSignal = signal<readonly OutboxEntry[]>([]);\n private readonly foreignSignal = signal<readonly OutboxEntry[]>([]);\n private readonly flushingSignal = signal(false);\n\n /** This app's queued work, oldest first — what it can actually replay. */\n readonly entries: Signal<readonly OutboxEntry[]> = this.mineSignal.asReadonly();\n /**\n * Work queued by another app on this page. Waiting, not stuck: it replays when its owner is\n * mounted. Surfaced so an app can render \"someone has unsent changes\" honestly rather than\n * reporting only what it can see of the queue.\n */\n readonly foreignEntries: Signal<readonly OutboxEntry[]> = this.foreignSignal.asReadonly();\n\n /**\n * Unsent work across the whole page, not just this app's.\n *\n * \"Are there unsent changes?\" is a page-wide question, and each app answering only for itself is\n * how the indicator was wrong before. Use {@link entries} when you need what *this* app owns.\n */\n readonly pendingCount = computed(() => this.mineSignal().length + this.foreignSignal().length);\n readonly hasPendingWork = computed(() => this.pendingCount() > 0);\n readonly isFlushing = this.flushingSignal.asReadonly();\n\n private outbox: Outbox | null = null;\n private loaded = false;\n\n private get queue(): Outbox {\n this.outbox ??= createOutbox({\n driver: this.options.driver,\n owner: this.options.owner,\n collection: OUTBOX_COLLECTION,\n ...(this.options.buildId === undefined ? {} : { buildId: this.options.buildId }),\n });\n return this.outbox;\n }\n\n /**\n * Registers how a mutation id replays. Called by `mutation()` at creation, which is why outbox\n * mutations must be constructed during app start-up rather than lazily inside a click handler —\n * otherwise a queued entry has nothing to replay into after a reload.\n */\n register(mutationId: string, runner: OutboxRunner): void {\n this.runners.set(mutationId, runner);\n }\n\n /** Rehydrates from storage. Safe to call repeatedly. */\n async load(): Promise<void> {\n if (this.loaded) return;\n this.loaded = true;\n await this.refresh();\n }\n\n async enqueue(entry: {\n mutationId: string;\n input: unknown;\n schemaVersion?: number;\n /** What readers should show for the records this entry changes, until it is confirmed. */\n optimistic?: OptimisticOverlay[];\n }): Promise<OutboxEntry> {\n await this.load();\n\n const id = await this.queue.enqueue(entry);\n await this.refresh();\n\n const queued = this.mineSignal().find((candidate) => candidate.id === id);\n if (!queued) throw new Error(`[skew/data] the outbox did not retain entry \"${id}\"`);\n return queued;\n }\n\n /**\n * Drains this app's queue in order.\n *\n * The rules — sequential, stop at the first failure, one drain per owner across every tab, give up\n * loudly — live in `@braidlabs/data`'s `drainOutbox`, because this service used to carry a second\n * copy of them and the two had already drifted apart on what to do with an entry nobody can\n * replay. What stays here is the Angular half: signals, and re-reading the shared store into them.\n *\n * Only this app's entries are touched. Another app's queued work is left exactly where it is.\n */\n async flush(): Promise<FlushResult> {\n await this.load();\n if (this.flushingSignal()) return { sent: 0, failed: 0, remaining: this.pendingCount(), skipped: true };\n\n this.flushingSignal.set(true);\n try {\n const result = await drainOutbox({\n outbox: this.queue,\n owner: this.options.owner,\n runnerFor: (mutationId) => this.runners.get(mutationId),\n maxAttempts: this.options.maxOutboxAttempts,\n ...(this.options.onOutboxError === undefined ? {} : { onError: this.options.onOutboxError }),\n });\n\n // Whatever happened — this app drained, or another tab did — the shared store moved, so the\n // signals have to be re-read from it rather than adjusted from the counts.\n await this.refresh();\n return { ...result, remaining: this.pendingCount() };\n } finally {\n this.flushingSignal.set(false);\n }\n }\n\n /**\n * Removes one entry, and with it the overlay derived from it.\n *\n * What a confirmation does, and equally what a rollback does — at this level they are the same\n * operation, which is the point of deriving the optimistic view rather than keeping an undo log.\n */\n async remove(id: string): Promise<void> {\n await this.load();\n await this.queue.remove(id);\n await this.refresh();\n }\n\n /**\n * Discards this app's queued work. Sign-out, or an explicit \"abandon my changes\".\n *\n * Another app's entries survive: discarding work this app never queued and cannot replay would be\n * deciding on someone else's behalf.\n */\n async clear(): Promise<void> {\n await this.load();\n for (const entry of await this.queue.mine()) await this.queue.remove(entry.id);\n await this.refresh();\n }\n\n /**\n * Re-reads the shared store into signals. The store is the truth; signals are a view of it.\n *\n * The optimistic overlay is rebuilt here too, from the same read. That is what makes the overlay\n * derived rather than remembered: a reload, another app's write, and a rolled-back entry all reach\n * the screen through this one path, because all three are just a different queue.\n */\n private async refresh(): Promise<void> {\n const [mine, foreign] = await Promise.all([this.queue.mine(), this.queue.foreign()]);\n this.mineSignal.set(mine);\n this.foreignSignal.set(foreign);\n this.pending.syncFromQueue([...mine, ...foreign]);\n }\n}\n","import { DestroyRef, Signal, computed, inject, signal } from '@angular/core';\nimport { CacheRegistry } from './cache-registry';\nimport type { EntityType } from './entity';\nimport { EntityStore } from './store';\n\n/**\n * A read that normalizes into the shared store.\n *\n * Angular's own `resource()` / `httpResource()` cannot do this: there is no\n * integration point that lets a third-party store observe what they fetched.\n * So this is a parallel primitive rather than an extension — duplication we\n * would happily delete if the framework grew the hook.\n */\n\nexport type QueryStatus = 'idle' | 'loading' | 'success' | 'error';\n\nexport interface QueryConfig<T> {\n /** Fetches the data. Anything promise-returning. */\n readonly loader: () => Promise<T>;\n /**\n * Entity types present in the response. Every matching record is written to\n * the store, so other views of the same entity update automatically.\n */\n readonly normalize?: EntityType<any> | ReadonlyArray<EntityType<any>>;\n /** Tags this query depends on; invalidating any of them re-runs it. */\n readonly tags?: () => readonly string[];\n /** Run immediately on creation. Default true. */\n readonly immediate?: boolean;\n}\n\nexport interface QueryRef<T> {\n readonly value: Signal<T | undefined>;\n readonly status: Signal<QueryStatus>;\n readonly error: Signal<unknown>;\n readonly isLoading: Signal<boolean>;\n /** Re-runs the loader, bypassing any tag bookkeeping. */\n reload(): Promise<void>;\n}\n\n/**\n * Creates a query. Must be called in an injection context.\n *\n * ```ts\n * readonly bulletins = query({\n * loader: () => firstValueFrom(this.http.get<Bulletin[]>('/api/bulletins')),\n * normalize: Bulletin,\n * tags: () => ['bulletins'],\n * });\n *\n * // Read through the store, not through `bulletins.value()`:\n * readonly rows = this.store.selectAll(Bulletin);\n * ```\n */\nexport function query<T>(config: QueryConfig<T>): QueryRef<T> {\n const store = inject(EntityStore);\n const registry = inject(CacheRegistry);\n const destroyRef = inject(DestroyRef);\n\n const value = signal<T | undefined>(undefined);\n const status = signal<QueryStatus>('idle');\n const error = signal<unknown>(undefined);\n\n /**\n * Guards against a slow earlier request resolving after a newer one and\n * overwriting fresher data — the classic out-of-order response bug.\n */\n let generation = 0;\n\n async function run(): Promise<void> {\n const current = ++generation;\n status.set('loading');\n error.set(undefined);\n\n try {\n const result = await config.loader();\n if (current !== generation) return;\n\n if (config.normalize) normalizeInto(store, config.normalize, result);\n\n value.set(result);\n status.set('success');\n } catch (caught) {\n if (current !== generation) return;\n error.set(caught);\n status.set('error');\n }\n }\n\n if (config.tags) {\n const dispose = registry.subscribe(config.tags, () => void run());\n destroyRef.onDestroy(dispose);\n }\n\n if (config.immediate ?? true) void run();\n\n return {\n value: value.asReadonly(),\n status: status.asReadonly(),\n error: error.asReadonly(),\n isLoading: computed(() => status() === 'loading'),\n reload: run,\n };\n}\n\n/**\n * Walks a payload and writes every recognised entity into the store.\n *\n * Handles the shapes real APIs return: a bare record, an array, or an envelope\n * such as `{ items: [...] }`. Anything unrecognised is left alone rather than\n * guessed at — a wrong write into the shared graph is worse than no write.\n */\nexport function normalizeInto(\n store: EntityStore,\n types: EntityType<any> | ReadonlyArray<EntityType<any>>,\n payload: unknown,\n): void {\n const list = Array.isArray(types) ? types : [types];\n for (const type of list) {\n for (const candidate of collectCandidates(payload)) {\n let id: string | undefined;\n try {\n id = type.key(candidate);\n } catch {\n continue; // not this type\n }\n if (typeof id === 'string' && id.length > 0) store.upsert(type, candidate);\n }\n }\n}\n\nfunction collectCandidates(payload: unknown): unknown[] {\n if (payload === null || payload === undefined) return [];\n if (Array.isArray(payload)) return payload.filter(isRecord);\n if (!isRecord(payload)) return [];\n\n // A single record, plus one level of common envelope shapes.\n const nested = Object.values(payload).filter(Array.isArray).flat().filter(isRecord);\n return [payload, ...nested];\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n","import { Signal, computed, inject, signal } from '@angular/core';\nimport { CacheRegistry } from './cache-registry';\nimport { OutboxService } from './outbox';\nimport { EntityStore, type StoreTransaction } from './store';\nimport { PendingWrites, recordOverlays, toOutboxOverlay, type EntityOverlay } from './overlay';\nimport type { EntityType } from './entity';\n\n/**\n * A write, with the three things every application ends up reimplementing:\n * optimistic application, rollback on failure, and durability.\n *\n * All three are one mechanism here: the **queue**. A write is queued before it\n * is sent, every reader derives `confirmed ⊕ pending` from that queue, and\n * settling it — confirmed or abandoned — is deleting the entry. There is no undo\n * log, because there is nothing to undo: nothing was written to the confirmed\n * graph in the first place.\n *\n * That is a change from the obvious design, and the reason is what the obvious\n * design cannot do. An optimistic patch applied to the store with an undo log in\n * memory is invisible to the other apps on the page and does not survive a\n * reload — so a user who queues a change offline and refreshes sees the *old*\n * value while their edit sits in storage waiting to send.\n */\n\nexport type MutationStatus = 'idle' | 'pending' | 'success' | 'error';\n\n/**\n * The server accepted the write and stored something else.\n *\n * Not a failure — servers normalize, resolve, and apply rules the client does\n * not know about — which is exactly why it is so easy to ship a UI that swaps\n * the value under whoever just typed it.\n */\nexport interface MutationConflict {\n /** The record as the screen showed it: confirmed, with this write's prediction applied. */\n expected: unknown;\n /** The record the server reported. */\n actual: unknown;\n /** The predicted fields the server disagreed about. */\n paths: readonly string[];\n /** Which record this is about. */\n entity: { typeName: string; id: string };\n}\n\n/**\n * What to do when the server disagrees.\n *\n * `'raise'` by default. The stored record becomes the server's value either way — you cannot make a\n * server hold your value without another mutation — so the only question is whether the user is\n * told, and the silent answer is a support ticket about someone's money.\n */\nexport type ConflictPolicy = 'raise' | 'accept' | ((conflict: MutationConflict) => unknown);\n\nexport interface MutationConfig<TInput, TResult> {\n /**\n * Stable identifier. Required when `durability: 'outbox'`, because a queued\n * entry outlives the closure and must find its operation again by name after\n * a reload.\n */\n readonly id?: string;\n readonly operation: (input: TInput) => Promise<TResult>;\n /**\n * Describes the change locally before the server has agreed.\n *\n * Still a callback taking a transaction, and still written exactly as it was —\n * but the transaction now **records** rather than writes. `tx.patch(Bulletin,\n * id, { status: 'published' })` was always a description; recording it is what\n * lets the same description survive a reload, which a closure never could.\n */\n readonly optimistic?: (tx: StoreTransaction, input: TInput) => void;\n /**\n * Normalizes the server's response back into the store — the confirmed half.\n *\n * Receives the value chosen by `onConflict` when a resolver returned one, so a\n * team that reconciles a disagreement writes the reconciled record rather than\n * having to re-apply it afterwards.\n */\n readonly onSuccess?: (store: EntityStore, result: TResult, input: TInput) => void;\n /** Tags to mark stale once the write lands. */\n readonly invalidates?: (input: TInput, result?: TResult) => readonly string[];\n /**\n * `'outbox'` persists the mutation so it survives a reload and replays when\n * connectivity returns. `'memory'` (default) fails fast.\n *\n * The overlay is derived from the queue either way. The difference is what\n * rebuilds it: a persisted entry can be read back after a reload, and an\n * in-memory one correctly dies with the page that made it.\n */\n readonly durability?: 'memory' | 'outbox';\n /** Payload contract version, carried on queued entries. */\n readonly schemaVersion?: number;\n /** Defaults to `'raise'`. */\n readonly onConflict?: ConflictPolicy;\n}\n\nexport interface MutationRef<TInput, TResult> {\n mutate(input: TInput): Promise<TResult>;\n readonly status: Signal<MutationStatus>;\n readonly error: Signal<unknown>;\n readonly isPending: Signal<boolean>;\n /** True while this mutation's prediction is on screen and unconfirmed. */\n readonly hasPendingWrite: Signal<boolean>;\n /** The last disagreement, under the `'raise'` policy. Cleared by the next attempt. */\n readonly conflict: Signal<MutationConflict | null>;\n /** Dismisses a raised conflict, once the user has seen it. */\n acknowledgeConflict(): void;\n}\n\n/**\n * Creates a mutation. Must be called in an injection context.\n *\n * ```ts\n * readonly publish = mutation({\n * id: 'bulletin.publish',\n * operation: (b: Bulletin) => firstValueFrom(this.http.post(`/api/bulletins/${b.id}/publish`, b)),\n * optimistic: (tx, b) => tx.patch(Bulletin, b.id, { status: 'published' }),\n * invalidates: (b) => [tag.entity(Bulletin, b.id), 'bulletins'],\n * durability: 'outbox',\n * schemaVersion: 41,\n * });\n * ```\n */\nexport function mutation<TInput, TResult>(\n config: MutationConfig<TInput, TResult>,\n): MutationRef<TInput, TResult> {\n const store = inject(EntityStore);\n const registry = inject(CacheRegistry);\n const outbox = inject(OutboxService);\n const pending = inject(PendingWrites);\n\n const durability = config.durability ?? 'memory';\n if (durability === 'outbox' && !config.id) {\n throw new TypeError(\n '[skew/data] a mutation with durability: \"outbox\" requires a stable `id`. ' +\n 'Queued entries outlive the closure and are matched back to their operation by id.',\n );\n }\n\n const status = signal<MutationStatus>('idle');\n const error = signal<unknown>(undefined);\n const conflict = signal<MutationConflict | null>(null);\n const applied = signal(0);\n\n // Replay path. Registered eagerly so a queue rehydrated at start-up has\n // somewhere to go — this is why outbox mutations must be created during\n // bootstrap rather than lazily inside a handler.\n if (durability === 'outbox' && config.id) {\n outbox.register(config.id, async (input) => config.operation(input as TInput));\n }\n\n async function mutate(input: TInput): Promise<TResult> {\n status.set('pending');\n error.set(undefined);\n conflict.set(null);\n\n const recorded = config.optimistic\n ? recordOverlays((tx) => config.optimistic?.(tx, input))\n : { overlays: [] as EntityOverlay[], types: new Map<string, EntityType<unknown>>() };\n\n // Applied synchronously and only here: the confirmed graph is never touched by a prediction.\n let groupId = `local:${crypto.randomUUID()}`;\n pending.add(groupId, recorded.overlays, { durable: durability === 'outbox' });\n if (recorded.overlays.length > 0) applied.update((n) => n + 1);\n\n // What the screen showed, captured before the server answers, so a conflict is measured against\n // the prediction rather than against whatever the record has become since.\n const expected = new Map<string, unknown>();\n for (const overlay of recorded.overlays) {\n const type = recorded.types.get(overlay.typeName);\n if (type) expected.set(overlayId(overlay), store.peek(type, overlay.id));\n }\n\n // Queued *before* sending, not on failure. The entry is what readers overlay, so a write only\n // recorded once it fails is a write nobody can see while it is in flight — and one that a crash\n // mid-request loses entirely.\n let entryId: string | null = null;\n if (durability === 'outbox' && config.id) {\n const entry = await outbox.enqueue({\n mutationId: config.id,\n input,\n schemaVersion: config.schemaVersion ?? 1,\n optimistic: recorded.overlays.map(toOutboxOverlay),\n });\n entryId = entry.id;\n // Re-keyed so the queue's own copy of this group replaces it rather than doubling it.\n pending.rename(groupId, entry.id);\n groupId = entry.id;\n }\n\n try {\n const result = await config.operation(input);\n\n const found = detectConflict(recorded, expected, result, store);\n let confirmed: TResult = result;\n if (found) {\n const policy = config.onConflict ?? 'raise';\n if (typeof policy === 'function') confirmed = policy(found) as TResult;\n else if (policy === 'raise') conflict.set(found);\n }\n\n config.onSuccess?.(store, confirmed, input);\n\n // The overlay lifts only after the confirmed value is in the store, so the value on screen\n // never drops back to the old one for a frame between the two.\n await settle(entryId, groupId);\n\n const tags = config.invalidates?.(input, result);\n if (tags?.length) registry.invalidate(...tags);\n\n status.set('success');\n return result;\n } catch (caught) {\n if (entryId) {\n // Keep the prediction and keep the entry: from the user's point of view the change happened,\n // and the queue is what will make that true when the network returns.\n status.set('success');\n return undefined as TResult;\n }\n\n // Rollback is deletion. Nothing was written to the confirmed graph, so there is nothing to\n // restore and no undo log that could have gone stale while the request was in flight.\n await settle(null, groupId);\n error.set(caught);\n status.set('error');\n throw caught;\n }\n }\n\n /** Drops the group, and the entry behind it when there is one. */\n async function settle(entryId: string | null, groupId: string): Promise<void> {\n if (entryId) await outbox.remove(entryId);\n pending.settle(groupId);\n applied.update((n) => (n > 0 ? n - 1 : 0));\n }\n\n return {\n mutate,\n status: status.asReadonly(),\n error: error.asReadonly(),\n isPending: computed(() => status() === 'pending'),\n hasPendingWrite: computed(() => applied() > 0),\n conflict: conflict.asReadonly(),\n acknowledgeConflict: () => conflict.set(null),\n };\n}\n\nconst overlayId = (overlay: EntityOverlay) => `${overlay.typeName}#${overlay.id}`;\n\n/**\n * Finds the disagreement, if the server's answer is one that can disagree.\n *\n * A conflict is only decidable when the response *is* the record: an operation resolving with\n * `void`, an id, or a receipt has not contradicted anything, and reporting one would be inventing a\n * disagreement out of a shape mismatch.\n */\nfunction detectConflict(\n recorded: { overlays: readonly EntityOverlay[]; types: Map<string, EntityType<unknown>> },\n expected: Map<string, unknown>,\n result: unknown,\n store: EntityStore,\n): MutationConflict | null {\n if (result === null || typeof result !== 'object') return null;\n\n for (const overlay of recorded.overlays) {\n if (overlay.removed) continue;\n\n const type = recorded.types.get(overlay.typeName);\n if (!type || !describes(type, overlay.id, result)) continue;\n\n const predicted = (expected.get(overlayId(overlay)) ?? store.peekConfirmed(type, overlay.id)) as\n | Record<string, unknown>\n | undefined;\n const actual = result as Record<string, unknown>;\n\n const paths = Object.keys(overlay.patch).filter((path) => !same(predicted?.[path], actual[path]));\n if (paths.length > 0) {\n return {\n expected: predicted,\n actual: result,\n paths,\n entity: { typeName: overlay.typeName, id: overlay.id },\n };\n }\n }\n\n return null;\n}\n\n/** Whether the response is this record. A `key` that throws on a foreign shape is a \"no\". */\nfunction describes(type: EntityType<unknown>, id: string, result: unknown): boolean {\n try {\n return type.key(result) === id;\n } catch {\n return false;\n }\n}\n\n/**\n * Structural equality, by serialization — adequate because both sides are values that came off the\n * wire moments ago, and a deep-equality helper would be code defending against inputs this path\n * cannot receive.\n */\nfunction same(a: unknown, b: unknown): boolean {\n return a === b || JSON.stringify(a) === JSON.stringify(b);\n}\n","import {\n DOCUMENT,\n type EnvironmentProviders,\n PLATFORM_ID,\n inject,\n makeEnvironmentProviders,\n provideEnvironmentInitializer,\n} from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\nimport { DATA_OPTIONS, type DataOptionsInput, resolveDataOptions } from './config';\nimport { OutboxService } from './outbox';\n\n/**\n * Enables the data layer.\n *\n * ```ts\n * provideSkewData({\n * owner: 'bulletin',\n * persistOutbox: true,\n * buildId: BUILD_ID,\n * onOutboxError: (message, detail) => telemetry.error(message, detail),\n * });\n * ```\n */\nexport function provideSkewData(input: DataOptionsInput = {}): EnvironmentProviders {\n const options = resolveDataOptions(input);\n\n return makeEnvironmentProviders([\n { provide: DATA_OPTIONS, useValue: options },\n provideEnvironmentInitializer(() => {\n const outbox = inject(OutboxService);\n const isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n if (!isBrowser) return;\n\n // Rehydrate immediately: work queued before the last reload should reach\n // the server as soon as this build is running, not on the next mutation.\n void outbox.load().then(() => outbox.flush());\n\n if (options.flushOnReconnect) {\n const view = inject(DOCUMENT).defaultView;\n view?.addEventListener('online', () => void outbox.flush());\n }\n }),\n ]);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"braidlabs-angular-data.mjs","names":[],"sources":["../../../../../libs/angular/data/src/lib/entity.ts","../../../../../libs/angular/data/src/lib/overlay.ts","../../../../../libs/angular/data/src/lib/store.ts","../../../../../libs/angular/data/src/lib/cache-registry.ts","../../../../../libs/angular/data/src/lib/config.ts","../../../../../libs/angular/data/src/lib/outbox.ts","../../../../../libs/angular/data/src/lib/query.ts","../../../../../libs/angular/data/src/lib/mutation.ts","../../../../../libs/angular/data/src/lib/provide.ts"],"sourcesContent":["/**\n * Entity identity.\n *\n * The gap this closes: `httpResource()` and friends are *per-call* caches. Two\n * requests that return the same record produce two independent copies, and\n * updating one leaves the other stale. Deduplicating them requires knowing that\n * both payloads describe the same thing — which requires a declared identity.\n */\n\nexport interface EntityType<T> {\n /** Stable name, used for store partitioning and tag generation. */\n readonly name: string;\n /** Extracts the primary key. Must be stable and collision-free per type. */\n readonly key: (value: T) => string;\n}\n\nexport interface EntityDefinition<T> {\n readonly name: string;\n readonly key: (value: T) => string;\n}\n\n/**\n * Declares an entity type.\n *\n * ```ts\n * export const Bulletin = entity<Bulletin>({ name: 'bulletin', key: (b) => b.id });\n * ```\n *\n * Declare each type once and export it — identity is only useful if every\n * query and mutation agrees on it.\n */\nexport function entity<T>(definition: EntityDefinition<T>): EntityType<T> {\n if (!definition.name) {\n throw new TypeError('[skew/data] entity() requires a name');\n }\n if (typeof definition.key !== 'function') {\n throw new TypeError(`[skew/data] entity \"${definition.name}\" requires a key function`);\n }\n return { name: definition.name, key: definition.key };\n}\n\n/** Tag helpers, so invalidation reads the same way everywhere. */\nexport const tag = {\n /** A single record: `bulletin#42`. */\n entity<T>(type: EntityType<T>, id: string): string {\n return `${type.name}#${id}`;\n },\n /** Every record of a type: `bulletin#*`. */\n all<T>(type: EntityType<T>): string {\n return `${type.name}#*`;\n },\n /** An arbitrary named collection or view. */\n collection(name: string): string {\n return name;\n },\n} as const;\n","import { Injectable, Signal, computed, signal } from '@angular/core';\nimport type { OptimisticOverlay, QueuedEntry } from '@braidlabs/data';\nimport type { EntityType } from './entity';\nimport type { StoreTransaction } from './store';\n\n/**\n * The optimistic overlay: what the queue predicts, applied over the confirmed graph.\n *\n * view(record) = confirmed(record) ⊕ pending(record)\n *\n * The important word is *derived*. This used to be an apply-then-undo: a write patched the store,\n * an undo log remembered the previous values, and a failure restored them. Two things went wrong\n * with that, and both are the kind of bug nobody reports as one:\n *\n * 1. **The overlay and the queue could disagree.** A queued mutation lives in storage; its\n * optimistic effect lived in memory. Reload the page and the work was still queued while the\n * screen showed the old value — the user's edit apparently lost, then reappearing later when the\n * queue drained.\n * 2. **Only the app that wrote could see it.** The queue is shared per origin; an undo log is not.\n *\n * Deriving fixes both at once. Rollback becomes deleting an entry, a reload rebuilds the overlay\n * from the queue, and another app's unsent edits show up here because they are in the same store.\n */\n\n/** One record's worth of prediction, in the entity store's terms. */\nexport interface EntityOverlay {\n readonly typeName: string;\n readonly id: string;\n /** Applied over the confirmed record. Empty for a removal. */\n readonly patch: Record<string, unknown>;\n /** True when the write deletes the record — absence a patch cannot express. */\n readonly removed?: boolean;\n}\n\n/**\n * Overlays that belong to one write, so settling or rolling back is one operation.\n *\n * `durable` marks the groups the queue owns: those are replaced wholesale on every re-read of the\n * queue, which is what makes storage the source of truth rather than this signal. Groups that are\n * not durable belong to `durability: 'memory'` mutations — nothing persisted them, so nothing can\n * rebuild them, and they correctly die with the page.\n */\ninterface OverlayGroup {\n readonly id: string;\n readonly durable: boolean;\n readonly overlays: readonly EntityOverlay[];\n}\n\n@Injectable({ providedIn: 'root' })\nexport class PendingWrites {\n private readonly groups = signal<readonly OverlayGroup[]>([]);\n\n /** Every pending overlay on the page, in the order the writes were made. */\n readonly overlays: Signal<readonly EntityOverlay[]> = computed(() =>\n this.groups().flatMap((group) => group.overlays),\n );\n\n /** Whether anything on screen is showing work the server has not confirmed. */\n readonly hasPending = computed(() => this.groups().length > 0);\n\n /**\n * Records a write's prediction.\n *\n * Applied synchronously, before the entry reaches storage: a UI that waits for IndexedDB to show\n * what the user just typed is a UI that flickers. Storage remains the truth — `syncFromQueue`\n * replaces this the moment the queue is next read.\n */\n add(id: string, overlays: readonly EntityOverlay[], options: { durable: boolean }): void {\n if (overlays.length === 0) return;\n this.groups.update((groups) => [\n ...groups.filter((group) => group.id !== id),\n { id, durable: options.durable, overlays },\n ]);\n }\n\n /** Drops a group: a confirmation, or a rollback. There is no difference at this level. */\n settle(id: string): void {\n this.groups.update((groups) => groups.filter((group) => group.id !== id));\n }\n\n /** Re-keys a group once its entry id is known, so the queue's copy replaces the same group. */\n rename(from: string, to: string): void {\n this.groups.update((groups) =>\n groups.map((group) => (group.id === from ? { ...group, id: to, durable: true } : group)),\n );\n }\n\n /**\n * Rebuilds every durable group from the queue.\n *\n * This is the derivation. Called on every outbox refresh — including the one after rehydrating\n * from storage, which is how a reload gets the overlay back without anything in memory having\n * survived, and how one app comes to show another's unsent edits.\n */\n syncFromQueue(entries: readonly QueuedEntry[]): void {\n const fromQueue = entries\n .filter((entry) => (entry.optimistic ?? []).length > 0)\n .map((entry) => ({\n id: entry.id,\n durable: true,\n overlays: (entry.optimistic ?? []).map(fromOutboxOverlay),\n }));\n\n this.groups.update((groups) => [...groups.filter((group) => !group.durable), ...fromQueue]);\n }\n\n /** Sign-out, and tests. */\n clear(): void {\n this.groups.set([]);\n }\n}\n\n/** `bulletin#42` — the same key `tag.entity` produces, so one write's tag and overlay agree. */\nexport function overlayKey(typeName: string, id: string): string {\n return `${typeName}#${id}`;\n}\n\nexport function toOutboxOverlay(overlay: EntityOverlay): OptimisticOverlay {\n return {\n key: overlayKey(overlay.typeName, overlay.id),\n patch: overlay.patch,\n ...(overlay.removed ? { removed: true } : {}),\n };\n}\n\n/**\n * Splits at the *first* `#`, because an id may contain one and a type name may not.\n */\nexport function fromOutboxOverlay(overlay: OptimisticOverlay): EntityOverlay {\n const at = overlay.key.indexOf('#');\n return {\n typeName: at === -1 ? overlay.key : overlay.key.slice(0, at),\n id: at === -1 ? '' : overlay.key.slice(at + 1),\n patch: overlay.patch,\n ...(overlay.removed ? { removed: true } : {}),\n };\n}\n\n/** Applies the overlays for one record, in order. `undefined` means pending removal. */\nexport function applyOverlays<T>(\n confirmed: T | undefined,\n overlays: readonly EntityOverlay[],\n): T | undefined {\n let value = confirmed;\n for (const overlay of overlays) {\n value = overlay.removed ? undefined : ({ ...(value ?? {}), ...overlay.patch } as T);\n }\n return value;\n}\n\n/**\n * Runs an `optimistic(tx, input)` callback against a transaction that **records** rather than\n * writes, turning the callback into overlay data.\n *\n * This is what lets the existing config surface stay exactly as it is while the mechanism\n * underneath changes: `tx.patch(Bulletin, id, { status: 'published' })` describes a patch perfectly\n * well, and a closure was never the part that mattered — it just happened to be how the description\n * was expressed. Recording it makes the same description survive a reload, which the closure could\n * never do.\n */\nexport interface RecordedWrites {\n readonly overlays: EntityOverlay[];\n /**\n * The entity types the callback named, by name.\n *\n * Kept beside the overlays rather than inside them because a type carries a `key` function, and a\n * function cannot be persisted — the overlays go to storage, this map stays in the closure that is\n * waiting on the server.\n */\n readonly types: Map<string, EntityType<unknown>>;\n}\n\nexport function recordOverlays(apply: (tx: StoreTransaction) => void): RecordedWrites {\n const recorded: EntityOverlay[] = [];\n const types = new Map<string, EntityType<unknown>>();\n\n const remember = <T>(type: EntityType<T>) => types.set(type.name, type as EntityType<unknown>);\n\n const tx: StoreTransaction = {\n upsert<T>(type: EntityType<T>, values: T | readonly T[]) {\n remember(type);\n const list = Array.isArray(values) ? (values as readonly T[]) : [values as T];\n for (const value of list) {\n recorded.push({\n typeName: type.name,\n id: type.key(value),\n patch: value as unknown as Record<string, unknown>,\n });\n }\n },\n patch<T>(type: EntityType<T>, id: string, partial: Partial<T>) {\n remember(type);\n recorded.push({ typeName: type.name, id, patch: partial as Record<string, unknown> });\n },\n remove<T>(type: EntityType<T>, id: string) {\n remember(type);\n recorded.push({ typeName: type.name, id, patch: {}, removed: true });\n },\n };\n\n apply(tx);\n return { overlays: recorded, types };\n}\n","import { Injectable, Signal, computed, inject, signal } from '@angular/core';\nimport type { EntityType } from './entity';\nimport { PendingWrites, applyOverlays, type EntityOverlay } from './overlay';\n\n/**\n * The normalized entity store.\n *\n * Components must read from *here*, not from the response object a query\n * resolved. This is the part teams get wrong: if a component holds the parsed\n * response, writing an updated record into the store changes nothing it can\n * observe, and normalization buys you exactly nothing.\n *\n * ✗ bulletins = this.list.value(); // a private copy that drifts\n * ✓ bulletins = store.selectAll(Bulletin); // a view of the shared graph\n *\n * **Reads are the view, not the confirmed graph.** Every read here returns\n * `confirmed ⊕ pending`: what the server has agreed to, with the queue's\n * predictions applied on top. Writes go only to the confirmed half — an\n * optimistic change is a queued entry, never a write here — which is why a\n * failed mutation needs no undo log and a reload shows the user their unsent\n * edit instead of the value they replaced. See `overlay.ts`.\n */\n\ntype EntityTable = ReadonlyMap<string, unknown>;\ntype StoreState = ReadonlyMap<string, EntityTable>;\n\n/** Records applied within a transaction, so they can be undone. */\ninterface UndoEntry {\n readonly typeName: string;\n readonly id: string;\n /** `undefined` means \"did not exist\", so undo removes it. */\n readonly previous: unknown | undefined;\n}\n\n/**\n * A batch of writes that can be rolled back wholesale.\n *\n * Optimistic updates need this: the write happens before the server has agreed,\n * so a failure must restore precisely what was there — not \"roughly re-fetch\".\n */\nexport interface StoreTransaction {\n upsert<T>(type: EntityType<T>, values: T | readonly T[]): void;\n patch<T>(type: EntityType<T>, id: string, partial: Partial<T>): void;\n remove<T>(type: EntityType<T>, id: string): void;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class EntityStore {\n private readonly state = signal<StoreState>(new Map());\n private readonly pending = inject(PendingWrites);\n\n /**\n * Memoized per `type#id`. Without this, calling `select()` inside a template\n * would mint a fresh computed on every change-detection pass — a subtle and\n * expensive footgun.\n */\n private readonly selectCache = new Map<string, Signal<unknown>>();\n private readonly allCache = new Map<string, Signal<readonly unknown[]>>();\n\n /** A single record as this page shows it: confirmed, with any pending write applied. */\n select<T>(type: EntityType<T>, id: string): Signal<T | undefined> {\n const cacheKey = `${type.name}#${id}`;\n const existing = this.selectCache.get(cacheKey);\n if (existing) return existing as Signal<T | undefined>;\n\n const derived = computed(() => this.view<T>(type, id));\n this.selectCache.set(cacheKey, derived);\n return derived;\n }\n\n /**\n * Every record of a type, in insertion order, with pending writes applied.\n *\n * A pending record nothing has confirmed yet still appears, and a pending removal does not: a\n * list that omits what the user just created, or keeps what they just deleted, is the same bug\n * seen from either end.\n */\n selectAll<T>(type: EntityType<T>): Signal<readonly T[]> {\n const existing = this.allCache.get(type.name);\n if (existing) return existing as Signal<readonly T[]>;\n\n const derived = computed(() => {\n const table = this.state().get(type.name);\n const overlaid = this.pending.overlays().filter((overlay) => overlay.typeName === type.name);\n const ids = new Set([...(table?.keys() ?? []), ...overlaid.map((overlay) => overlay.id)]);\n\n const rows: T[] = [];\n for (const id of ids) {\n const value = this.view<T>(type, id);\n if (value !== undefined) rows.push(value);\n }\n return rows;\n });\n this.allCache.set(type.name, derived);\n return derived;\n }\n\n /**\n * A filtered view. Not memoized by predicate — call it once and keep the\n * result, as you would any other `computed`.\n */\n query<T>(type: EntityType<T>, predicate: (value: T) => boolean): Signal<readonly T[]> {\n const all = this.selectAll(type);\n return computed(() => all().filter(predicate));\n }\n\n /**\n * Reads a record without subscribing. For use inside effects and handlers.\n *\n * The same value `select` would give, pending writes included — a read that disagrees with the\n * subscribed read depending on which one you happened to call is a trap, not an optimization.\n * {@link peekConfirmed} is the one that skips the overlay.\n */\n peek<T>(type: EntityType<T>, id: string): T | undefined {\n return this.view<T>(type, id);\n }\n\n /**\n * The record as the server last reported it, ignoring anything queued.\n *\n * What a conflict is measured against: \"the server disagreed with you\" is a claim about the\n * prediction, so comparing against a value that already includes the prediction would answer a\n * different question.\n */\n peekConfirmed<T>(type: EntityType<T>, id: string): T | undefined {\n return this.state().get(type.name)?.get(id) as T | undefined;\n }\n\n upsert<T>(type: EntityType<T>, values: T | readonly T[]): void {\n this.applyWrites(this.toWrites(type, values));\n }\n\n patch<T>(type: EntityType<T>, id: string, partial: Partial<T>): void {\n const current = this.peek(type, id);\n if (current === undefined) return;\n this.applyWrites([{ typeName: type.name, id, value: { ...current, ...partial } }]);\n }\n\n remove<T>(type: EntityType<T>, id: string): void {\n this.applyWrites([{ typeName: type.name, id, value: undefined }]);\n }\n\n /** Empties a type, or the whole store. Mainly for sign-out and tests. */\n clear<T>(type?: EntityType<T>): void {\n if (!type) {\n this.state.set(new Map());\n return;\n }\n const next = new Map(this.state());\n next.delete(type.name);\n this.state.set(next);\n }\n\n /**\n * Applies writes that can be undone as a unit.\n *\n * ```ts\n * const tx = store.transaction();\n * tx.apply((t) => t.patch(Bulletin, id, { status: 'published' }));\n * // …server says no…\n * tx.rollback();\n * ```\n */\n transaction(): {\n apply(fn: (tx: StoreTransaction) => void): void;\n rollback(): void;\n committed: boolean;\n } {\n const undoLog: UndoEntry[] = [];\n let rolledBack = false;\n\n const record = (typeName: string, id: string): void => {\n // Only the *first* prior value matters; later writes in the same\n // transaction are intermediate states we never want to restore to.\n if (undoLog.some((entry) => entry.typeName === typeName && entry.id === id)) return;\n undoLog.push({ typeName, id, previous: this.state().get(typeName)?.get(id) });\n };\n\n const tx: StoreTransaction = {\n upsert: <T>(type: EntityType<T>, values: T | readonly T[]) => {\n const writes = this.toWrites(type, values);\n for (const write of writes) record(write.typeName, write.id);\n this.applyWrites(writes);\n },\n patch: <T>(type: EntityType<T>, id: string, partial: Partial<T>) => {\n record(type.name, id);\n const current = this.peek(type, id);\n if (current === undefined) return;\n this.applyWrites([{ typeName: type.name, id, value: { ...current, ...partial } }]);\n },\n remove: <T>(type: EntityType<T>, id: string) => {\n record(type.name, id);\n this.applyWrites([{ typeName: type.name, id, value: undefined }]);\n },\n };\n\n return {\n apply(fn) {\n fn(tx);\n },\n rollback: () => {\n if (rolledBack) return;\n rolledBack = true;\n this.applyWrites(\n // Restore in reverse so overlapping writes unwind correctly.\n [...undoLog].reverse().map((entry) => ({\n typeName: entry.typeName,\n id: entry.id,\n value: entry.previous,\n })),\n );\n },\n get committed() {\n return !rolledBack;\n },\n };\n }\n\n // --- internals ---------------------------------------------------------\n\n /** `confirmed ⊕ pending` for one record. The single place the two halves meet. */\n private view<T>(type: EntityType<T>, id: string): T | undefined {\n const confirmed = this.state().get(type.name)?.get(id) as T | undefined;\n const overlays: EntityOverlay[] = this.pending\n .overlays()\n .filter((overlay) => overlay.typeName === type.name && overlay.id === id);\n\n if (overlays.length === 0) return confirmed;\n return applyOverlays(confirmed, overlays);\n }\n\n private toWrites<T>(\n type: EntityType<T>,\n values: T | readonly T[],\n ): Array<{ typeName: string; id: string; value: unknown }> {\n const list = Array.isArray(values) ? (values as readonly T[]) : [values as T];\n return list.map((value) => ({\n typeName: type.name,\n id: type.key(value),\n value,\n }));\n }\n\n /**\n * Single write path, so every mutation produces exactly one signal\n * notification regardless of how many records it touched.\n */\n private applyWrites(writes: ReadonlyArray<{ typeName: string; id: string; value: unknown }>): void {\n if (writes.length === 0) return;\n\n const next = new Map(this.state());\n const touched = new Map<string, Map<string, unknown>>();\n\n for (const { typeName, id, value } of writes) {\n let table = touched.get(typeName);\n if (!table) {\n table = new Map(next.get(typeName) ?? []);\n touched.set(typeName, table);\n }\n if (value === undefined) table.delete(id);\n else table.set(id, value);\n }\n\n for (const [typeName, table] of touched) next.set(typeName, table);\n this.state.set(next);\n }\n}\n","import { Injectable } from '@angular/core';\n\n/**\n * Tag-based invalidation.\n *\n * Tags rather than TTLs, because a time-to-live is a guess about when data went\n * stale, whereas a mutation *knows*. `bulletin#42` and `bulletins` are claims\n * about what a query depends on; invalidating a tag re-runs exactly the queries\n * that said they cared.\n *\n * Wildcards are supported in one direction only: invalidating `bulletin#*`\n * matches every `bulletin#…` subscriber. A subscriber may also register the\n * wildcard itself, in which case any invalidation of that type reaches it.\n */\n@Injectable({ providedIn: 'root' })\nexport class CacheRegistry {\n private readonly subscribers = new Map<\n symbol,\n { readonly getTags: () => readonly string[]; readonly notify: () => void }\n >();\n\n /**\n * Registers interest in a set of tags. Returns a disposer.\n *\n * Tags are supplied as a getter and re-read on each invalidation, so a query\n * whose tags depend on signals (a route param, a filter) stays correct\n * without having to re-subscribe.\n */\n subscribe(getTags: () => readonly string[], notify: () => void): () => void {\n const token = Symbol('skew-cache-subscriber');\n this.subscribers.set(token, { getTags, notify });\n return () => void this.subscribers.delete(token);\n }\n\n /**\n * Marks tags stale, notifying every matching subscriber.\n *\n * Notification is synchronous but failure-isolated: one subscriber throwing\n * must not prevent the others from refreshing.\n */\n invalidate(...tags: readonly string[]): void {\n if (tags.length === 0) return;\n for (const subscriber of [...this.subscribers.values()]) {\n let interested = false;\n try {\n interested = subscriber.getTags().some((owned) => tags.some((t) => tagsMatch(owned, t)));\n } catch {\n // A tag getter that throws (a signal read outside its context, say)\n // must not take down invalidation for everyone else.\n interested = false;\n }\n if (!interested) continue;\n try {\n subscriber.notify();\n } catch {\n // A refresh that throws is that query's problem, not this loop's.\n }\n }\n }\n\n /** Subscriber count. Diagnostics and tests only. */\n get size(): number {\n return this.subscribers.size;\n }\n}\n\n/**\n * `bulletin#*` matches `bulletin#42` in either position, so a mutation can\n * invalidate a whole type without enumerating ids, and a list query can\n * subscribe to a whole type without knowing which ids it will receive.\n */\nexport function tagsMatch(a: string, b: string): boolean {\n if (a === b) return true;\n const wildcardOf = (tag: string) => (tag.endsWith('#*') ? tag.slice(0, -1) : null);\n\n const aPrefix = wildcardOf(a);\n if (aPrefix && b.startsWith(aPrefix)) return true;\n\n const bPrefix = wildcardOf(b);\n if (bPrefix && a.startsWith(bPrefix)) return true;\n\n return false;\n}\n","import { InjectionToken } from '@angular/core';\nimport { indexedDbRecordDriver, memoryRecordDriver, type RecordDriver } from '@braidlabs/data';\n\nexport interface DataOptions {\n /**\n * Where the outbox is stored. Shared across every app on the origin.\n *\n * A record-oriented driver rather than a key/value store, and that is the correctness fix: the\n * queue used to live under a single key, so appending meant read-modify-write and two apps\n * interleaving lost each other's entries.\n */\n readonly driver: RecordDriver;\n /**\n * Which application this is, stamped onto every entry it queues.\n *\n * The store is shared, so ownership is what keeps one app from acting on another's queued work.\n * Only the owner replays an entry; everyone else leaves it alone.\n */\n readonly owner: string;\n /** Attempts before a queued entry is abandoned. Default 5. */\n readonly maxOutboxAttempts: number;\n /**\n * Reports outbox trouble: an abandoned entry, a mutation this build no longer has, a failed write.\n *\n * Silent by default, but wiring it to telemetry is how you find out that users are losing queued\n * work — the failure is otherwise invisible, because the user already navigated away believing it\n * saved.\n */\n readonly onOutboxError?: (message: string, detail?: unknown) => void;\n /** Flush the outbox when the browser reports it is back online. Default true. */\n readonly flushOnReconnect: boolean;\n readonly buildId?: string;\n /**\n * Whose queue this is: pass `() => tenancy.partition()` on any device more than one person signs\n * in to, and list the outbox collection in that tenancy's `collections`. Queued work then belongs\n * to the person who queued it, the next person's flush never replays it under their session, and\n * sign-out purges it. Without it the queue is device-wide, which suits a single-user device only.\n *\n * Give the tenancy and this the same `driver` (the purge clears the tenancy's driver only), and\n * call `OutboxService.flush()` after each sign-in so the service reads the new person's queue.\n */\n readonly partition?: string | (() => string);\n}\n\nexport const DATA_OPTIONS = new InjectionToken<DataOptions>('SKEW_DATA_OPTIONS');\n\nexport interface DataOptionsInput {\n /**\n * Identifies this application in the shared outbox. **Required when persisting.**\n *\n * Not defaulted, deliberately. A default would be the same string in every app on the page, which\n * is precisely the collision ownership exists to prevent — and it would fail silently, in\n * production, on someone's unsent work.\n */\n readonly owner?: string;\n /** Supply storage directly. Overrides `persistOutbox`. */\n readonly driver?: RecordDriver;\n readonly maxOutboxAttempts?: number;\n readonly onOutboxError?: DataOptions['onOutboxError'];\n readonly flushOnReconnect?: boolean;\n /**\n * Persist the outbox in IndexedDB, so queued work survives a reload.\n *\n * Off by default: queued work is then lost on reload, which is only appropriate when every\n * mutation is fire-and-forget.\n */\n readonly persistOutbox?: boolean;\n /** Stamped onto persisted envelopes for attribution. */\n readonly buildId?: string;\n /** Whose queue this is. See {@link DataOptions.partition}; set it on any shared device. */\n readonly partition?: string | (() => string);\n /** Database name when persisting. Defaults to `skew-data`. */\n readonly database?: string;\n /**\n * Extra collections to create alongside the outbox.\n *\n * Declared up front because IndexedDB can only create object stores during a version upgrade —\n * a collection used but not declared fails at the first read, not at configuration time.\n */\n readonly collections?: readonly string[];\n}\n\nexport const OUTBOX_COLLECTION = 'outbox';\n\n/**\n * Whether this environment can persist at all.\n *\n * False during server rendering, where there is no IndexedDB — and no point in one, since the\n * outbox exists to survive a *client* reload. Falling back to memory keeps `persistOutbox: true` a\n * safe thing to write in a config shared by both bootstraps, which is where it naturally goes.\n */\nfunction canPersist(): boolean {\n return typeof globalThis.indexedDB !== 'undefined';\n}\n\nfunction persistentDriver(input: DataOptionsInput) {\n return indexedDbRecordDriver({\n collections: [OUTBOX_COLLECTION, ...(input.collections ?? [])],\n ...(input.database === undefined ? {} : { database: input.database }),\n });\n}\n\nexport function resolveDataOptions(input: DataOptionsInput = {}): DataOptions {\n const persisting = input.persistOutbox === true || input.driver !== undefined;\n\n if (persisting && !input.owner) {\n throw new Error(\n '[skew/data] a persisted outbox needs `owner`: a string identifying this application.\\n' +\n 'The outbox is shared across every app on the origin, and ownership is what stops one app ' +\n \"from replaying or discarding another's queued mutations. Defaulting it would put every app \" +\n 'under the same name, which is the collision it exists to prevent.',\n );\n }\n\n return {\n driver: input.driver ?? (input.persistOutbox && canPersist() ? persistentDriver(input) : memoryRecordDriver()),\n owner: input.owner ?? 'app',\n maxOutboxAttempts: input.maxOutboxAttempts ?? 5,\n ...(input.onOutboxError === undefined ? {} : { onOutboxError: input.onOutboxError }),\n flushOnReconnect: input.flushOnReconnect ?? true,\n ...(input.buildId === undefined ? {} : { buildId: input.buildId }),\n ...(input.partition === undefined ? {} : { partition: input.partition }),\n };\n}\n","import { Injectable, Signal, computed, inject, signal } from '@angular/core';\nimport { createOutbox, drainOutbox, type Outbox, type OptimisticOverlay, type QueuedEntry } from '@braidlabs/data';\nimport { DATA_OPTIONS, OUTBOX_COLLECTION } from './config';\nimport { PendingWrites } from './overlay';\n\n/**\n * The durable mutation outbox, as an Angular service over `@braidlabs/data`.\n *\n * This is the piece that cannot be built with in-flight request machinery. A mutation queued while\n * offline has to survive a page reload — and after a reload there is no pending `Promise` to retry,\n * no `HttpRequest` to intercept, and no closure left alive. Only something persisted can be\n * replayed.\n *\n * Which forces one API constraint: an outbox mutation must have a stable `id`, because the\n * *operation* is a closure and closures do not serialise. On replay we look the operation up by id.\n *\n * **The store is shared and the entries are owned.** Persistence lives in `@braidlabs/data`, one\n * record per entry, in a store every app on the origin reads. This service replays only what it\n * owns. Previously the queue was a single blob under one key, so a second app overwrote the first's\n * and then dropped its entries for having no registered runner — silently, unless `onOutboxError`\n * happened to be wired.\n */\n\n/** A queued entry as this app sees it. */\nexport type OutboxEntry = QueuedEntry;\n\nexport type OutboxRunner = (input: unknown, entry: OutboxEntry) => Promise<unknown>;\n\nexport interface FlushResult {\n sent: number;\n failed: number;\n remaining: number;\n /**\n * True when this call did no work because someone else was already flushing — another tab, or a\n * re-entrant call.\n *\n * Distinct from `sent === 0`, which also happens when the flush *ran* and everything failed.\n * Conflating them makes a failing server look like a busy tab, which sends whoever is debugging\n * in exactly the wrong direction.\n */\n skipped: boolean;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class OutboxService {\n private readonly options = inject(DATA_OPTIONS);\n private readonly pending = inject(PendingWrites);\n private readonly runners = new Map<string, OutboxRunner>();\n\n private readonly mineSignal = signal<readonly OutboxEntry[]>([]);\n private readonly foreignSignal = signal<readonly OutboxEntry[]>([]);\n private readonly flushingSignal = signal(false);\n\n /** This app's queued work, oldest first — what it can actually replay. */\n readonly entries: Signal<readonly OutboxEntry[]> = this.mineSignal.asReadonly();\n /**\n * Work queued by another app on this page. Waiting, not stuck: it replays when its owner is\n * mounted. Surfaced so an app can render \"someone has unsent changes\" honestly rather than\n * reporting only what it can see of the queue.\n */\n readonly foreignEntries: Signal<readonly OutboxEntry[]> = this.foreignSignal.asReadonly();\n\n /**\n * Unsent work across the whole page, not just this app's.\n *\n * \"Are there unsent changes?\" is a page-wide question, and each app answering only for itself is\n * how the indicator was wrong before. Use {@link entries} when you need what *this* app owns.\n */\n readonly pendingCount = computed(() => this.mineSignal().length + this.foreignSignal().length);\n readonly hasPendingWork = computed(() => this.pendingCount() > 0);\n readonly isFlushing = this.flushingSignal.asReadonly();\n\n private outbox: Outbox | null = null;\n private loaded = false;\n\n private get queue(): Outbox {\n this.outbox ??= createOutbox({\n driver: this.options.driver,\n owner: this.options.owner,\n collection: OUTBOX_COLLECTION,\n ...(this.options.buildId === undefined ? {} : { buildId: this.options.buildId }),\n ...(this.options.partition === undefined ? {} : { partition: this.options.partition }),\n });\n return this.outbox;\n }\n\n /**\n * Registers how a mutation id replays. Called by `mutation()` at creation, which is why outbox\n * mutations must be constructed during app start-up rather than lazily inside a click handler —\n * otherwise a queued entry has nothing to replay into after a reload.\n */\n register(mutationId: string, runner: OutboxRunner): void {\n this.runners.set(mutationId, runner);\n }\n\n /** Rehydrates from storage. Safe to call repeatedly. */\n async load(): Promise<void> {\n if (this.loaded) return;\n // Marked only once the read succeeds: a partitioned queue cannot be read while nobody is signed\n // in, and a load that failed then must be retried after sign-in, not skipped forever.\n await this.refresh();\n this.loaded = true;\n }\n\n async enqueue(entry: {\n mutationId: string;\n input: unknown;\n schemaVersion?: number;\n /** What readers should show for the records this entry changes, until it is confirmed. */\n optimistic?: OptimisticOverlay[];\n }): Promise<OutboxEntry> {\n await this.load();\n\n const id = await this.queue.enqueue(entry);\n await this.refresh();\n\n const queued = this.mineSignal().find((candidate) => candidate.id === id);\n if (!queued) throw new Error(`[skew/data] the outbox did not retain entry \"${id}\"`);\n return queued;\n }\n\n /**\n * Drains this app's queue in order.\n *\n * The rules — sequential, stop at the first failure, one drain per owner across every tab, give up\n * loudly — live in `@braidlabs/data`'s `drainOutbox`, because this service used to carry a second\n * copy of them and the two had already drifted apart on what to do with an entry nobody can\n * replay. What stays here is the Angular half: signals, and re-reading the shared store into them.\n *\n * Only this app's entries are touched. Another app's queued work is left exactly where it is.\n */\n async flush(): Promise<FlushResult> {\n await this.load();\n if (this.flushingSignal()) return { sent: 0, failed: 0, remaining: this.pendingCount(), skipped: true };\n\n this.flushingSignal.set(true);\n try {\n const result = await drainOutbox({\n outbox: this.queue,\n owner: this.options.owner,\n runnerFor: (mutationId) => this.runners.get(mutationId),\n maxAttempts: this.options.maxOutboxAttempts,\n ...(this.options.onOutboxError === undefined ? {} : { onError: this.options.onOutboxError }),\n });\n\n // Whatever happened — this app drained, or another tab did — the shared store moved, so the\n // signals have to be re-read from it rather than adjusted from the counts.\n await this.refresh();\n return { ...result, remaining: this.pendingCount() };\n } finally {\n this.flushingSignal.set(false);\n }\n }\n\n /**\n * Removes one entry, and with it the overlay derived from it.\n *\n * What a confirmation does, and equally what a rollback does — at this level they are the same\n * operation, which is the point of deriving the optimistic view rather than keeping an undo log.\n */\n async remove(id: string): Promise<void> {\n await this.load();\n await this.queue.remove(id);\n await this.refresh();\n }\n\n /**\n * Discards this app's queued work. Sign-out, or an explicit \"abandon my changes\".\n *\n * Another app's entries survive: discarding work this app never queued and cannot replay would be\n * deciding on someone else's behalf.\n */\n async clear(): Promise<void> {\n await this.load();\n for (const entry of await this.queue.mine()) await this.queue.remove(entry.id);\n await this.refresh();\n }\n\n /**\n * Re-reads the shared store into signals. The store is the truth; signals are a view of it.\n *\n * The optimistic overlay is rebuilt here too, from the same read. That is what makes the overlay\n * derived rather than remembered: a reload, another app's write, and a rolled-back entry all reach\n * the screen through this one path, because all three are just a different queue.\n */\n private async refresh(): Promise<void> {\n const [mine, foreign] = await Promise.all([this.queue.mine(), this.queue.foreign()]);\n this.mineSignal.set(mine);\n this.foreignSignal.set(foreign);\n this.pending.syncFromQueue([...mine, ...foreign]);\n }\n}\n","import { DestroyRef, Signal, computed, inject, signal } from '@angular/core';\nimport { CacheRegistry } from './cache-registry';\nimport type { EntityType } from './entity';\nimport { EntityStore } from './store';\n\n/**\n * A read that normalizes into the shared store.\n *\n * Angular's own `resource()` / `httpResource()` cannot do this: there is no\n * integration point that lets a third-party store observe what they fetched.\n * So this is a parallel primitive rather than an extension — duplication we\n * would happily delete if the framework grew the hook.\n */\n\nexport type QueryStatus = 'idle' | 'loading' | 'success' | 'error';\n\nexport interface QueryConfig<T> {\n /** Fetches the data. Anything promise-returning. */\n readonly loader: () => Promise<T>;\n /**\n * Entity types present in the response. Every matching record is written to\n * the store, so other views of the same entity update automatically.\n */\n readonly normalize?: EntityType<any> | ReadonlyArray<EntityType<any>>;\n /** Tags this query depends on; invalidating any of them re-runs it. */\n readonly tags?: () => readonly string[];\n /** Run immediately on creation. Default true. */\n readonly immediate?: boolean;\n}\n\nexport interface QueryRef<T> {\n readonly value: Signal<T | undefined>;\n readonly status: Signal<QueryStatus>;\n readonly error: Signal<unknown>;\n readonly isLoading: Signal<boolean>;\n /** Re-runs the loader, bypassing any tag bookkeeping. */\n reload(): Promise<void>;\n}\n\n/**\n * Creates a query. Must be called in an injection context.\n *\n * ```ts\n * readonly bulletins = query({\n * loader: () => firstValueFrom(this.http.get<Bulletin[]>('/api/bulletins')),\n * normalize: Bulletin,\n * tags: () => ['bulletins'],\n * });\n *\n * // Read through the store, not through `bulletins.value()`:\n * readonly rows = this.store.selectAll(Bulletin);\n * ```\n */\nexport function query<T>(config: QueryConfig<T>): QueryRef<T> {\n const store = inject(EntityStore);\n const registry = inject(CacheRegistry);\n const destroyRef = inject(DestroyRef);\n\n const value = signal<T | undefined>(undefined);\n const status = signal<QueryStatus>('idle');\n const error = signal<unknown>(undefined);\n\n /**\n * Guards against a slow earlier request resolving after a newer one and\n * overwriting fresher data — the classic out-of-order response bug.\n */\n let generation = 0;\n\n async function run(): Promise<void> {\n const current = ++generation;\n status.set('loading');\n error.set(undefined);\n\n try {\n const result = await config.loader();\n if (current !== generation) return;\n\n if (config.normalize) normalizeInto(store, config.normalize, result);\n\n value.set(result);\n status.set('success');\n } catch (caught) {\n if (current !== generation) return;\n error.set(caught);\n status.set('error');\n }\n }\n\n if (config.tags) {\n const dispose = registry.subscribe(config.tags, () => void run());\n destroyRef.onDestroy(dispose);\n }\n\n if (config.immediate ?? true) void run();\n\n return {\n value: value.asReadonly(),\n status: status.asReadonly(),\n error: error.asReadonly(),\n isLoading: computed(() => status() === 'loading'),\n reload: run,\n };\n}\n\n/**\n * Walks a payload and writes every recognised entity into the store.\n *\n * Handles the shapes real APIs return: a bare record, an array, or an envelope\n * such as `{ items: [...] }`. Anything unrecognised is left alone rather than\n * guessed at — a wrong write into the shared graph is worse than no write.\n */\nexport function normalizeInto(\n store: EntityStore,\n types: EntityType<any> | ReadonlyArray<EntityType<any>>,\n payload: unknown,\n): void {\n const list = Array.isArray(types) ? types : [types];\n for (const type of list) {\n for (const candidate of collectCandidates(payload)) {\n let id: string | undefined;\n try {\n id = type.key(candidate);\n } catch {\n continue; // not this type\n }\n if (typeof id === 'string' && id.length > 0) store.upsert(type, candidate);\n }\n }\n}\n\nfunction collectCandidates(payload: unknown): unknown[] {\n if (payload === null || payload === undefined) return [];\n if (Array.isArray(payload)) return payload.filter(isRecord);\n if (!isRecord(payload)) return [];\n\n // A single record, plus one level of common envelope shapes.\n const nested = Object.values(payload).filter(Array.isArray).flat().filter(isRecord);\n return [payload, ...nested];\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n","import { Signal, computed, inject, signal } from '@angular/core';\nimport { CacheRegistry } from './cache-registry';\nimport { OutboxService } from './outbox';\nimport { EntityStore, type StoreTransaction } from './store';\nimport { PendingWrites, recordOverlays, toOutboxOverlay, type EntityOverlay } from './overlay';\nimport type { EntityType } from './entity';\n\n/**\n * A write, with the three things every application ends up reimplementing:\n * optimistic application, rollback on failure, and durability.\n *\n * All three are one mechanism here: the **queue**. A write is queued before it\n * is sent, every reader derives `confirmed ⊕ pending` from that queue, and\n * settling it — confirmed or abandoned — is deleting the entry. There is no undo\n * log, because there is nothing to undo: nothing was written to the confirmed\n * graph in the first place.\n *\n * That is a change from the obvious design, and the reason is what the obvious\n * design cannot do. An optimistic patch applied to the store with an undo log in\n * memory is invisible to the other apps on the page and does not survive a\n * reload — so a user who queues a change offline and refreshes sees the *old*\n * value while their edit sits in storage waiting to send.\n */\n\nexport type MutationStatus = 'idle' | 'pending' | 'success' | 'error';\n\n/**\n * The server accepted the write and stored something else.\n *\n * Not a failure — servers normalize, resolve, and apply rules the client does\n * not know about — which is exactly why it is so easy to ship a UI that swaps\n * the value under whoever just typed it.\n */\nexport interface MutationConflict {\n /** The record as the screen showed it: confirmed, with this write's prediction applied. */\n expected: unknown;\n /** The record the server reported. */\n actual: unknown;\n /** The predicted fields the server disagreed about. */\n paths: readonly string[];\n /** Which record this is about. */\n entity: { typeName: string; id: string };\n}\n\n/**\n * What to do when the server disagrees.\n *\n * `'raise'` by default. The stored record becomes the server's value either way — you cannot make a\n * server hold your value without another mutation — so the only question is whether the user is\n * told, and the silent answer is a support ticket about someone's money.\n */\nexport type ConflictPolicy = 'raise' | 'accept' | ((conflict: MutationConflict) => unknown);\n\nexport interface MutationConfig<TInput, TResult> {\n /**\n * Stable identifier. Required when `durability: 'outbox'`, because a queued\n * entry outlives the closure and must find its operation again by name after\n * a reload.\n */\n readonly id?: string;\n readonly operation: (input: TInput) => Promise<TResult>;\n /**\n * Describes the change locally before the server has agreed.\n *\n * Still a callback taking a transaction, and still written exactly as it was —\n * but the transaction now **records** rather than writes. `tx.patch(Bulletin,\n * id, { status: 'published' })` was always a description; recording it is what\n * lets the same description survive a reload, which a closure never could.\n */\n readonly optimistic?: (tx: StoreTransaction, input: TInput) => void;\n /**\n * Normalizes the server's response back into the store — the confirmed half.\n *\n * Receives the value chosen by `onConflict` when a resolver returned one, so a\n * team that reconciles a disagreement writes the reconciled record rather than\n * having to re-apply it afterwards.\n */\n readonly onSuccess?: (store: EntityStore, result: TResult, input: TInput) => void;\n /** Tags to mark stale once the write lands. */\n readonly invalidates?: (input: TInput, result?: TResult) => readonly string[];\n /**\n * `'outbox'` persists the mutation so it survives a reload and replays when\n * connectivity returns. `'memory'` (default) fails fast.\n *\n * The overlay is derived from the queue either way. The difference is what\n * rebuilds it: a persisted entry can be read back after a reload, and an\n * in-memory one correctly dies with the page that made it.\n */\n readonly durability?: 'memory' | 'outbox';\n /** Payload contract version, carried on queued entries. */\n readonly schemaVersion?: number;\n /** Defaults to `'raise'`. */\n readonly onConflict?: ConflictPolicy;\n}\n\nexport interface MutationRef<TInput, TResult> {\n mutate(input: TInput): Promise<TResult>;\n readonly status: Signal<MutationStatus>;\n readonly error: Signal<unknown>;\n readonly isPending: Signal<boolean>;\n /** True while this mutation's prediction is on screen and unconfirmed. */\n readonly hasPendingWrite: Signal<boolean>;\n /** The last disagreement, under the `'raise'` policy. Cleared by the next attempt. */\n readonly conflict: Signal<MutationConflict | null>;\n /** Dismisses a raised conflict, once the user has seen it. */\n acknowledgeConflict(): void;\n}\n\n/**\n * Creates a mutation. Must be called in an injection context.\n *\n * ```ts\n * readonly publish = mutation({\n * id: 'bulletin.publish',\n * operation: (b: Bulletin) => firstValueFrom(this.http.post(`/api/bulletins/${b.id}/publish`, b)),\n * optimistic: (tx, b) => tx.patch(Bulletin, b.id, { status: 'published' }),\n * invalidates: (b) => [tag.entity(Bulletin, b.id), 'bulletins'],\n * durability: 'outbox',\n * schemaVersion: 41,\n * });\n * ```\n */\nexport function mutation<TInput, TResult>(\n config: MutationConfig<TInput, TResult>,\n): MutationRef<TInput, TResult> {\n const store = inject(EntityStore);\n const registry = inject(CacheRegistry);\n const outbox = inject(OutboxService);\n const pending = inject(PendingWrites);\n\n const durability = config.durability ?? 'memory';\n if (durability === 'outbox' && !config.id) {\n throw new TypeError(\n '[skew/data] a mutation with durability: \"outbox\" requires a stable `id`. ' +\n 'Queued entries outlive the closure and are matched back to their operation by id.',\n );\n }\n\n const status = signal<MutationStatus>('idle');\n const error = signal<unknown>(undefined);\n const conflict = signal<MutationConflict | null>(null);\n const applied = signal(0);\n\n // Replay path. Registered eagerly so a queue rehydrated at start-up has\n // somewhere to go — this is why outbox mutations must be created during\n // bootstrap rather than lazily inside a handler.\n if (durability === 'outbox' && config.id) {\n outbox.register(config.id, async (input) => config.operation(input as TInput));\n }\n\n async function mutate(input: TInput): Promise<TResult> {\n status.set('pending');\n error.set(undefined);\n conflict.set(null);\n\n const recorded = config.optimistic\n ? recordOverlays((tx) => config.optimistic?.(tx, input))\n : { overlays: [] as EntityOverlay[], types: new Map<string, EntityType<unknown>>() };\n\n // Applied synchronously and only here: the confirmed graph is never touched by a prediction.\n let groupId = `local:${crypto.randomUUID()}`;\n pending.add(groupId, recorded.overlays, { durable: durability === 'outbox' });\n if (recorded.overlays.length > 0) applied.update((n) => n + 1);\n\n // What the screen showed, captured before the server answers, so a conflict is measured against\n // the prediction rather than against whatever the record has become since.\n const expected = new Map<string, unknown>();\n for (const overlay of recorded.overlays) {\n const type = recorded.types.get(overlay.typeName);\n if (type) expected.set(overlayId(overlay), store.peek(type, overlay.id));\n }\n\n // Queued *before* sending, not on failure. The entry is what readers overlay, so a write only\n // recorded once it fails is a write nobody can see while it is in flight — and one that a crash\n // mid-request loses entirely.\n let entryId: string | null = null;\n if (durability === 'outbox' && config.id) {\n const entry = await outbox.enqueue({\n mutationId: config.id,\n input,\n schemaVersion: config.schemaVersion ?? 1,\n optimistic: recorded.overlays.map(toOutboxOverlay),\n });\n entryId = entry.id;\n // Re-keyed so the queue's own copy of this group replaces it rather than doubling it.\n pending.rename(groupId, entry.id);\n groupId = entry.id;\n }\n\n try {\n const result = await config.operation(input);\n\n const found = detectConflict(recorded, expected, result, store);\n let confirmed: TResult = result;\n if (found) {\n const policy = config.onConflict ?? 'raise';\n if (typeof policy === 'function') confirmed = policy(found) as TResult;\n else if (policy === 'raise') conflict.set(found);\n }\n\n config.onSuccess?.(store, confirmed, input);\n\n // The overlay lifts only after the confirmed value is in the store, so the value on screen\n // never drops back to the old one for a frame between the two.\n await settle(entryId, groupId);\n\n const tags = config.invalidates?.(input, result);\n if (tags?.length) registry.invalidate(...tags);\n\n status.set('success');\n return result;\n } catch (caught) {\n if (entryId) {\n // Keep the prediction and keep the entry: from the user's point of view the change happened,\n // and the queue is what will make that true when the network returns.\n status.set('success');\n return undefined as TResult;\n }\n\n // Rollback is deletion. Nothing was written to the confirmed graph, so there is nothing to\n // restore and no undo log that could have gone stale while the request was in flight.\n await settle(null, groupId);\n error.set(caught);\n status.set('error');\n throw caught;\n }\n }\n\n /** Drops the group, and the entry behind it when there is one. */\n async function settle(entryId: string | null, groupId: string): Promise<void> {\n if (entryId) await outbox.remove(entryId);\n pending.settle(groupId);\n applied.update((n) => (n > 0 ? n - 1 : 0));\n }\n\n return {\n mutate,\n status: status.asReadonly(),\n error: error.asReadonly(),\n isPending: computed(() => status() === 'pending'),\n hasPendingWrite: computed(() => applied() > 0),\n conflict: conflict.asReadonly(),\n acknowledgeConflict: () => conflict.set(null),\n };\n}\n\nconst overlayId = (overlay: EntityOverlay) => `${overlay.typeName}#${overlay.id}`;\n\n/**\n * Finds the disagreement, if the server's answer is one that can disagree.\n *\n * A conflict is only decidable when the response *is* the record: an operation resolving with\n * `void`, an id, or a receipt has not contradicted anything, and reporting one would be inventing a\n * disagreement out of a shape mismatch.\n */\nfunction detectConflict(\n recorded: { overlays: readonly EntityOverlay[]; types: Map<string, EntityType<unknown>> },\n expected: Map<string, unknown>,\n result: unknown,\n store: EntityStore,\n): MutationConflict | null {\n if (result === null || typeof result !== 'object') return null;\n\n for (const overlay of recorded.overlays) {\n if (overlay.removed) continue;\n\n const type = recorded.types.get(overlay.typeName);\n if (!type || !describes(type, overlay.id, result)) continue;\n\n const predicted = (expected.get(overlayId(overlay)) ?? store.peekConfirmed(type, overlay.id)) as\n | Record<string, unknown>\n | undefined;\n const actual = result as Record<string, unknown>;\n\n const paths = Object.keys(overlay.patch).filter((path) => !same(predicted?.[path], actual[path]));\n if (paths.length > 0) {\n return {\n expected: predicted,\n actual: result,\n paths,\n entity: { typeName: overlay.typeName, id: overlay.id },\n };\n }\n }\n\n return null;\n}\n\n/** Whether the response is this record. A `key` that throws on a foreign shape is a \"no\". */\nfunction describes(type: EntityType<unknown>, id: string, result: unknown): boolean {\n try {\n return type.key(result) === id;\n } catch {\n return false;\n }\n}\n\n/**\n * Structural equality, by serialization — adequate because both sides are values that came off the\n * wire moments ago, and a deep-equality helper would be code defending against inputs this path\n * cannot receive.\n */\nfunction same(a: unknown, b: unknown): boolean {\n return a === b || JSON.stringify(a) === JSON.stringify(b);\n}\n","import {\n DOCUMENT,\n type EnvironmentProviders,\n PLATFORM_ID,\n inject,\n makeEnvironmentProviders,\n provideEnvironmentInitializer,\n} from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\nimport { DATA_OPTIONS, type DataOptionsInput, resolveDataOptions } from './config';\nimport { OutboxService } from './outbox';\n\n/**\n * Enables the data layer.\n *\n * ```ts\n * provideSkewData({\n * owner: 'bulletin',\n * persistOutbox: true,\n * buildId: BUILD_ID,\n * onOutboxError: (message, detail) => telemetry.error(message, detail),\n * });\n * ```\n */\nexport function provideSkewData(input: DataOptionsInput = {}): EnvironmentProviders {\n const options = resolveDataOptions(input);\n\n return makeEnvironmentProviders([\n { provide: DATA_OPTIONS, useValue: options },\n provideEnvironmentInitializer(() => {\n const outbox = inject(OutboxService);\n const isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n if (!isBrowser) return;\n\n // Rehydrate immediately: work queued before the last reload should reach\n // the server as soon as this build is running, not on the next mutation.\n // A partitioned queue refuses to be read while nobody is signed in; that is reported, not left\n // as an unhandled rejection, and the app's flush after sign-in picks the queue up.\n const report = (error: unknown) =>\n options.onOutboxError?.(`[skew/data] the outbox could not be read: ${String(error)}`, error);\n void outbox.load().then(() => outbox.flush()).catch(report);\n\n if (options.flushOnReconnect) {\n const view = inject(DOCUMENT).defaultView;\n view?.addEventListener('online', () => void outbox.flush().catch(report));\n }\n }),\n ]);\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@braidlabs/angular-data",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "peerDependencies": {
5
5
  "@angular/core": "^22.0.0",
6
6
  "@angular/common": "^22.0.0",
7
- "@braidlabs/data": "^0.7.1"
7
+ "@braidlabs/data": "^0.8.0"
8
8
  },
9
9
  "sideEffects": false,
10
10
  "description": "Normalized entity store, tag invalidation, and a durable mutation outbox for Angular.",
@@ -282,6 +282,16 @@ export interface DataOptions {
282
282
  /** Flush the outbox when the browser reports it is back online. Default true. */
283
283
  readonly flushOnReconnect: boolean;
284
284
  readonly buildId?: string;
285
+ /**
286
+ * Whose queue this is: pass `() => tenancy.partition()` on any device more than one person signs
287
+ * in to, and list the outbox collection in that tenancy's `collections`. Queued work then belongs
288
+ * to the person who queued it, the next person's flush never replays it under their session, and
289
+ * sign-out purges it. Without it the queue is device-wide, which suits a single-user device only.
290
+ *
291
+ * Give the tenancy and this the same `driver` (the purge clears the tenancy's driver only), and
292
+ * call `OutboxService.flush()` after each sign-in so the service reads the new person's queue.
293
+ */
294
+ readonly partition?: string | (() => string);
285
295
  }
286
296
  export declare const DATA_OPTIONS: InjectionToken<DataOptions>;
287
297
  export interface DataOptionsInput {
@@ -307,6 +317,8 @@ export interface DataOptionsInput {
307
317
  readonly persistOutbox?: boolean;
308
318
  /** Stamped onto persisted envelopes for attribution. */
309
319
  readonly buildId?: string;
320
+ /** Whose queue this is. See {@link DataOptions.partition}; set it on any shared device. */
321
+ readonly partition?: string | (() => string);
310
322
  /** Database name when persisting. Defaults to `skew-data`. */
311
323
  readonly database?: string;
312
324
  /**