@braidlabs/data 0.1.1

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.
Files changed (43) hide show
  1. package/package.json +41 -0
  2. package/src/index.d.ts +34 -0
  3. package/src/index.js +23 -0
  4. package/src/index.js.map +1 -0
  5. package/src/lib/adapters.d.ts +74 -0
  6. package/src/lib/adapters.js +10 -0
  7. package/src/lib/adapters.js.map +1 -0
  8. package/src/lib/events.d.ts +141 -0
  9. package/src/lib/events.js +337 -0
  10. package/src/lib/events.js.map +1 -0
  11. package/src/lib/flush.d.ts +49 -0
  12. package/src/lib/flush.js +60 -0
  13. package/src/lib/flush.js.map +1 -0
  14. package/src/lib/indexeddb-driver.d.ts +29 -0
  15. package/src/lib/indexeddb-driver.js +160 -0
  16. package/src/lib/indexeddb-driver.js.map +1 -0
  17. package/src/lib/intents.d.ts +83 -0
  18. package/src/lib/intents.js +136 -0
  19. package/src/lib/intents.js.map +1 -0
  20. package/src/lib/invalidation.d.ts +63 -0
  21. package/src/lib/invalidation.js +136 -0
  22. package/src/lib/invalidation.js.map +1 -0
  23. package/src/lib/locks.d.ts +43 -0
  24. package/src/lib/locks.js +78 -0
  25. package/src/lib/locks.js.map +1 -0
  26. package/src/lib/memory-driver.d.ts +10 -0
  27. package/src/lib/memory-driver.js +53 -0
  28. package/src/lib/memory-driver.js.map +1 -0
  29. package/src/lib/outbox.d.ts +146 -0
  30. package/src/lib/outbox.js +89 -0
  31. package/src/lib/outbox.js.map +1 -0
  32. package/src/lib/partitions.d.ts +70 -0
  33. package/src/lib/partitions.js +62 -0
  34. package/src/lib/partitions.js.map +1 -0
  35. package/src/lib/query.d.ts +235 -0
  36. package/src/lib/query.js +430 -0
  37. package/src/lib/query.js.map +1 -0
  38. package/src/lib/record-store.d.ts +125 -0
  39. package/src/lib/record-store.js +85 -0
  40. package/src/lib/record-store.js.map +1 -0
  41. package/src/lib/tenancy.d.ts +121 -0
  42. package/src/lib/tenancy.js +220 -0
  43. package/src/lib/tenancy.js.map +1 -0
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@braidlabs/data",
3
+ "version": "0.1.1",
4
+ "description": "Local-first client data layer: persistence-first, skew-tolerant across independently deployed readers.",
5
+ "keywords": [
6
+ "local-first",
7
+ "indexeddb",
8
+ "offline",
9
+ "outbox",
10
+ "micro-frontends",
11
+ "skew"
12
+ ],
13
+ "license": "MIT",
14
+ "sideEffects": false,
15
+ "type": "module",
16
+ "main": "./src/index.js",
17
+ "types": "./src/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./src/index.d.ts",
21
+ "default": "./src/index.js"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
25
+ "peerDependencies": {
26
+ "@braidlabs/skew": "^0.1.1"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/braidjs/braid.git",
34
+ "directory": "libs/data"
35
+ },
36
+ "homepage": "https://github.com/braidjs/braid#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/braidjs/braid/issues"
39
+ },
40
+ "module": "./src/index.js"
41
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @braidlabs/data — the local-first data layer.
3
+ *
4
+ * Persistence-first: IndexedDB is the source of truth and memory is a derived view. Every record is
5
+ * enveloped by `@braidlabs/skew` and projected to *this* reader's version on the way out, which is
6
+ * what lets independently deployed apps read the same store at different contract versions.
7
+ *
8
+ * Framework-neutral. Angular bindings live in `@braidlabs/angular-data`.
9
+ */
10
+ export { createRecordStore } from './lib/record-store.js';
11
+ export type { RecordStore, RecordStoreOptions, RecordDriver, StoredRecord, ProjectedRecord, } from './lib/record-store.js';
12
+ export { memoryRecordDriver } from './lib/memory-driver.js';
13
+ export { indexedDbRecordDriver } from './lib/indexeddb-driver.js';
14
+ export type { IndexedDbDriverOptions } from './lib/indexeddb-driver.js';
15
+ export { withLock, hasCrossContextLocks, outboxFlushLock } from './lib/locks.js';
16
+ export type { LockResult, LockOptions } from './lib/locks.js';
17
+ export { createDataClient } from './lib/query.js';
18
+ export type { DataClient, DataClientOptions, Query, QueryDefinition, QueryState, QueryStatus, Conflict, ConflictPolicy, MutationDefinition, MutationOutcome, MutationRegistration, } from './lib/query.js';
19
+ export { drainOutbox } from './lib/flush.js';
20
+ export type { DrainOptions, FlushResult, MutationRunner } from './lib/flush.js';
21
+ export { createInvalidator, sharedInvalidator, resetSharedInvalidators } from './lib/invalidation.js';
22
+ export type { Invalidator, InvalidationOptions, BroadcastChannelLike } from './lib/invalidation.js';
23
+ export { createOutbox, OutboxEntrySchema } from './lib/outbox.js';
24
+ export type { Outbox, OutboxOptions, OutboxEntry, QueuedEntry, OptimisticOverlay } from './lib/outbox.js';
25
+ export { createTenancy, partitionKey, TenancyRecordSchema } from './lib/tenancy.js';
26
+ export type { Tenancy, TenancyOptions, Principal, AdoptOptions } from './lib/tenancy.js';
27
+ export { copyPartition } from './lib/partitions.js';
28
+ export type { CopyPartitionOptions, CopyPartitionResult, PartitionConflictPolicy, } from './lib/partitions.js';
29
+ export { fromPull } from './lib/adapters.js';
30
+ export type { PullAdapter, PushAdapter, PushConnection, PushRecord, PushSink, CommandAdapter, } from './lib/adapters.js';
31
+ export { createEventBus } from './lib/events.js';
32
+ export type { EventBus, EventBusOptions, Channel, ChannelOptions, SubscribeOptions, BusEvent, Delivery, } from './lib/events.js';
33
+ export { createIntentRegistry, NoIntentHandlerError } from './lib/intents.js';
34
+ export type { IntentRegistry, IntentRegistryOptions, IntentHandlerOptions, IntentCandidate, IntentResult, ResolvePolicy, RaiseOptions, } from './lib/intents.js';
package/src/index.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @braidlabs/data — the local-first data layer.
3
+ *
4
+ * Persistence-first: IndexedDB is the source of truth and memory is a derived view. Every record is
5
+ * enveloped by `@braidlabs/skew` and projected to *this* reader's version on the way out, which is
6
+ * what lets independently deployed apps read the same store at different contract versions.
7
+ *
8
+ * Framework-neutral. Angular bindings live in `@braidlabs/angular-data`.
9
+ */
10
+ export { createRecordStore } from './lib/record-store.js';
11
+ export { memoryRecordDriver } from './lib/memory-driver.js';
12
+ export { indexedDbRecordDriver } from './lib/indexeddb-driver.js';
13
+ export { withLock, hasCrossContextLocks, outboxFlushLock } from './lib/locks.js';
14
+ export { createDataClient } from './lib/query.js';
15
+ export { drainOutbox } from './lib/flush.js';
16
+ export { createInvalidator, sharedInvalidator, resetSharedInvalidators } from './lib/invalidation.js';
17
+ export { createOutbox, OutboxEntrySchema } from './lib/outbox.js';
18
+ export { createTenancy, partitionKey, TenancyRecordSchema } from './lib/tenancy.js';
19
+ export { copyPartition } from './lib/partitions.js';
20
+ export { fromPull } from './lib/adapters.js';
21
+ export { createEventBus } from './lib/events.js';
22
+ export { createIntentRegistry, NoIntentHandlerError } from './lib/intents.js';
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/data/src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAS1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAGlE,OAAO,EAAE,QAAQ,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAGjF,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAelD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAG7C,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAGtG,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAGlE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAGpF,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAOpD,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAU7C,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAWjD,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,74 @@
1
+ import type { VersionedSchema } from '@braidlabs/skew';
2
+ /**
3
+ * Protocol adapters: the layer never learns how to fetch.
4
+ *
5
+ * Three shapes cover the four protocols teams actually have. Each is a function type rather than a
6
+ * class or a registration, because the smallest thing that can be substituted is the thing most
7
+ * likely to be substituted — a REST endpoint replaced by a GraphQL query should not be a change to
8
+ * this package.
9
+ *
10
+ * | Shape | Covers |
11
+ * | --------- | --------------------------------------- |
12
+ * | `Pull` | REST, RPC, GraphQL queries |
13
+ * | `Push` | GraphQL subscriptions, WebSockets, SSE |
14
+ * | `Command` | REST writes, RPC calls, GraphQL mutations |
15
+ */
16
+ /** Fetches one record. Cancellable, because a query that outlives its subscriber must stop. */
17
+ export type PullAdapter = (key: string, signal: AbortSignal) => Promise<unknown>;
18
+ /**
19
+ * Sends a write.
20
+ *
21
+ * Takes *input*, never a closure, and that constraint comes from the outbox rather than from taste:
22
+ * a queued command is replayed by a build that may not be the one that queued it.
23
+ */
24
+ export type CommandAdapter<TInput = unknown> = (input: TInput) => Promise<unknown>;
25
+ /**
26
+ * A record arriving from the server unbidden, and the key it belongs under.
27
+ *
28
+ * Push is the shape that bends the model — it is a third write source alongside optimistic and
29
+ * confirmed — so it is deliberately expressed as records handed to the layer rather than as a
30
+ * stream the layer subscribes to. Everything a push writes goes through the same enveloping path as
31
+ * every other write; otherwise a WebSocket update is the one record in the system with no version
32
+ * on it, which is exactly the record a fragment two majors behind will read.
33
+ */
34
+ export interface PushRecord {
35
+ key: string;
36
+ value: unknown;
37
+ /** Tags to mark stale, for queries that hold lists rather than this key. */
38
+ tags?: string[];
39
+ /**
40
+ * Which tenant partition this record belongs to. Defaults to the client's current one.
41
+ *
42
+ * One socket often carries events for more than one tenant — an advisor subscribed to every client
43
+ * they cover, a desk subscribed to every fund. Filing all of it under whichever tenant happened to
44
+ * be on screen would put one client's position in another's cache, so a stream that knows better
45
+ * says so.
46
+ */
47
+ partition?: string;
48
+ }
49
+ /** What a push adapter writes into. Supplied by the client; never implemented by callers. */
50
+ export interface PushSink {
51
+ /** Lands one record, enveloped and stamped, and refreshes readers of that key. */
52
+ receive(record: PushRecord): Promise<void>;
53
+ /** Reports a stream-level failure without tearing the subscription down. */
54
+ fail?(error: unknown): void;
55
+ }
56
+ /**
57
+ * Subscribes to a stream and writes what arrives into `sink`.
58
+ *
59
+ * Returns a disposer, or nothing when the `signal` is all the cleanup it needs.
60
+ */
61
+ export type PushAdapter = (sink: PushSink, signal: AbortSignal) => void | (() => void) | Promise<void | (() => void)>;
62
+ export interface PushConnection<T> {
63
+ schema: VersionedSchema<T>;
64
+ source: PushAdapter;
65
+ /** Applied to every record the stream produces, when the stream reports bare tags. */
66
+ tags?: (key: string) => string[];
67
+ }
68
+ /**
69
+ * Adapts a pull into a query's `fetch`.
70
+ *
71
+ * Trivial by design — the value is in the *shape* being named, so a team writing a second transport
72
+ * has something to conform to rather than a signature to reverse-engineer from a query definition.
73
+ */
74
+ export declare function fromPull(key: string, adapter: PullAdapter): (signal: AbortSignal) => Promise<unknown>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Adapts a pull into a query's `fetch`.
3
+ *
4
+ * Trivial by design — the value is in the *shape* being named, so a team writing a second transport
5
+ * has something to conform to rather than a signature to reverse-engineer from a query definition.
6
+ */
7
+ export function fromPull(key, adapter) {
8
+ return (signal) => adapter(key, signal);
9
+ }
10
+ //# sourceMappingURL=adapters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapters.js","sourceRoot":"","sources":["../../../../../libs/data/src/lib/adapters.ts"],"names":[],"mappings":"AA2EA;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,GAAW,EAAE,OAAoB;IACxD,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;AAC1C,CAAC"}
@@ -0,0 +1,141 @@
1
+ import type { VersionedSchema } from '@braidlabs/skew';
2
+ import { type FlushResult } from './flush.js';
3
+ import { type BroadcastChannelLike, type Invalidator } from './invalidation.js';
4
+ import type { RecordDriver } from './record-store.js';
5
+ import type { Conflict, ConflictPolicy } from './query.js';
6
+ import { type IntentHandlerOptions, type IntentRegistry, type IntentResult, type RaiseOptions } from './intents.js';
7
+ /**
8
+ * Eventing across independently deployed applications: channels, durable queues, and conflict.
9
+ *
10
+ * This lives in `@braidlabs/data` rather than in Braid because none of it is about composition — it is
11
+ * about a queue, a lock, and a versioned payload, which is what this package already is. A Module
12
+ * Federation deployment with no Braid at all should be able to use it, and Braid's context bus should
13
+ * be able to sit on top rather than beside it.
14
+ *
15
+ * **Almost nothing here is new.** Durability is the outbox, one record per entry. Draining is
16
+ * `drainOutbox`, with its ordering, its cross-context lock, and its give-up rule. Waking another
17
+ * realm is the invalidator. Surviving version skew is `versioned()`. The parts that are genuinely
18
+ * new are the two decisions below.
19
+ *
20
+ * **1. State is not an event.** FDC3 conflates them: `broadcast` sets a channel's current context
21
+ * *and* replays it to late joiners, so it means both "this happened" and "this is now true". That
22
+ * works for a selected row and fails for anything with a verb. Here `broadcast`/`state` is the
23
+ * former and `emit`/`addEventListener` is the latter.
24
+ *
25
+ * **2. Durable fan-out is one queue per consumer, not one queue with acknowledgements.** An
26
+ * at-least-once event is written once per registered consumer, into that consumer's own queue, and
27
+ * *deletion on success is the acknowledgement*. It costs N copies of the payload and buys three
28
+ * things: no new storage shape, no ack bookkeeping to keep consistent, and — the one that matters —
29
+ * a consumer whose handler keeps failing blocks only itself. A shared entry with an ack set gives
30
+ * every consumer the same head-of-line blocking.
31
+ */
32
+ export type Delivery = 'at-most-once' | 'at-least-once';
33
+ export interface BusEvent<T = unknown> {
34
+ /** Stable per emit, so an at-least-once consumer can dedupe if it must. */
35
+ eventId: string;
36
+ channel: string;
37
+ type: string;
38
+ payload: T;
39
+ /** ISO 8601. Used for the retention window. */
40
+ at: string;
41
+ /** Which application emitted it. */
42
+ source: string;
43
+ }
44
+ export interface EventBusOptions {
45
+ /**
46
+ * This application's identity, as the outbox's `owner` is. Durable delivery is addressed to it,
47
+ * and two applications sharing one name will replay each other's events.
48
+ */
49
+ consumer: string;
50
+ /** Tenant partition. Events are scoped like every other record. */
51
+ partition?: () => string;
52
+ /** Storage for durable channels. Without it, `at-least-once` is refused rather than downgraded. */
53
+ driver?: RecordDriver;
54
+ /**
55
+ * Every application that may consume a durable event on this origin.
56
+ *
57
+ * Fan-out writes one entry per name, so a queue cannot wait for an acknowledgement from a consumer
58
+ * it does not know exists. In a Braid deployment this comes from the manifests; anywhere else it is
59
+ * a list. A registered consumer that is not currently mounted is *waiting*, not gone.
60
+ */
61
+ consumers?: readonly string[];
62
+ /** Cross-context wake-up and page-scoped fan-out. Defaults to the shared one for the partition. */
63
+ invalidator?: Invalidator;
64
+ /** Origin-scope transport. Injectable for tests; defaults to a `BroadcastChannel`. */
65
+ broadcastChannel?: (name: string) => BroadcastChannelLike | undefined;
66
+ /** Attempts before a queued event is abandoned. */
67
+ maxAttempts?: number;
68
+ /** Never silent: retention drops and abandoned handlers are reported here. */
69
+ onEventError?: (message: string, detail?: unknown) => void;
70
+ }
71
+ export interface ChannelOptions {
72
+ /**
73
+ * `'page'` reaches every realm on this page; `'origin'` reaches every tab as well.
74
+ *
75
+ * Per channel rather than per bus, because the answer differs by channel on the same page: a
76
+ * selected row is page state, a sign-out is origin-wide. One global setting is how a UI selection
77
+ * ends up broadcast to five tabs.
78
+ */
79
+ scope?: 'page' | 'origin';
80
+ /** What to do when two publishers disagree about this channel's state. Defaults to `'raise'`. */
81
+ onConflict?: ConflictPolicy<unknown>;
82
+ /** How long a queued event stays deliverable. Defaults to one hour. */
83
+ ttlMs?: number;
84
+ /** Queue depth per consumer *on this channel*, after which the oldest is dropped loudly. Defaults to 500. */
85
+ maxDepth?: number;
86
+ /**
87
+ * Instances this channel per entity: a fund, a tenant, a portfolio, a conversation.
88
+ *
89
+ * `channel('selection', { entity: 'fund:f1' })` and `channel('selection', { entity: 'fund:f2' })`
90
+ * are two separate contexts that happen to share a shape. State does not cross between them,
91
+ * events are addressed to one of them, and an origin-scoped pair gets a transport each.
92
+ *
93
+ * **Deliberately not a `scope` value.** Reach and instance are different questions and a channel
94
+ * has to answer both: this fund's selection is page state, while this fund's orders want to reach
95
+ * every tab. Folding entity into `scope` would force one answer for both.
96
+ *
97
+ * Not a security boundary — that is what a partition is. Two entities in one partition are
98
+ * separated here for correctness and clarity, not for isolation; a different *tenant* belongs in a
99
+ * different partition, and then none of this applies because the storage differs too.
100
+ */
101
+ entity?: string;
102
+ }
103
+ export interface SubscribeOptions {
104
+ /** The contract version this subscriber speaks. Projected per subscriber, as the context bus does. */
105
+ as?: number;
106
+ signal?: AbortSignal;
107
+ consumer?: string;
108
+ }
109
+ export interface Channel<S = unknown> {
110
+ readonly name: string;
111
+ /** Sets this channel's current state. Last value wins; late subscribers receive it. */
112
+ broadcast(value: S): void;
113
+ /** The current state, projected to `as`. */
114
+ state(options?: {
115
+ as?: number;
116
+ }): S | undefined;
117
+ /** Subscribes to state. Called immediately with the current value when there is one. */
118
+ subscribe(listener: (value: S) => void, options?: SubscribeOptions): () => void;
119
+ /** Publishes an occurrence. Not replayed unless it is durable and unacknowledged. */
120
+ emit<T>(type: string, payload: T, options?: {
121
+ delivery?: Delivery;
122
+ }): Promise<void>;
123
+ /** Handles occurrences of one type. */
124
+ addEventListener<T>(type: string, handler: (event: BusEvent<T>) => void | Promise<void>, options?: SubscribeOptions): () => void;
125
+ /** The last state conflict raised on this channel, if any. */
126
+ conflict(): Conflict<unknown> | null;
127
+ acknowledgeConflict(): void;
128
+ }
129
+ export interface EventBus extends Pick<IntentRegistry, 'candidates'> {
130
+ channel<S = unknown>(name: string, options?: ChannelOptions): Channel<S>;
131
+ /** Registers a handler for an intent raised by any application on this page. */
132
+ addIntentListener<T>(intent: string, options: IntentHandlerOptions<T>): () => void;
133
+ /** Asks whoever can handle this to handle it. Resolution policy decides between candidates. */
134
+ raiseIntent<R = unknown>(intent: string, payload: unknown, options?: RaiseOptions): Promise<IntentResult<R>>;
135
+ /** Declares the contract a state key or event type carries, so delivery can project per reader. */
136
+ register<T>(type: string, schema: VersionedSchema<T>): void;
137
+ /** Drains this consumer's queue of durable events. Called on wake-up; call it after mounting. */
138
+ flush(): Promise<FlushResult>;
139
+ close(): void;
140
+ }
141
+ export declare function createEventBus(options: EventBusOptions): EventBus;