@durable-streams/state 0.2.9 → 0.3.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.
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+
3
+ //#region src/types.ts
4
+ /**
5
+ * Type guard to check if an event is a change event
6
+ */
7
+ function isChangeEvent(event) {
8
+ return event != null && `operation` in event.headers;
9
+ }
10
+ /**
11
+ * Type guard to check if an event is a control event
12
+ */
13
+ function isControlEvent(event) {
14
+ return event != null && `control` in event.headers;
15
+ }
16
+
17
+ //#endregion
18
+ //#region src/materialized-state.ts
19
+ /**
20
+ * MaterializedState maintains an in-memory view of state from change events.
21
+ *
22
+ * It organizes data by type, where each type contains a map of key -> value.
23
+ * This supports multi-type streams where different entity types can coexist.
24
+ */
25
+ var MaterializedState = class {
26
+ data;
27
+ constructor() {
28
+ this.data = new Map();
29
+ }
30
+ /**
31
+ * Apply a single change event to update the materialized state
32
+ */
33
+ apply(event) {
34
+ const { type, key, value, headers } = event;
35
+ let typeMap = this.data.get(type);
36
+ if (!typeMap) {
37
+ typeMap = new Map();
38
+ this.data.set(type, typeMap);
39
+ }
40
+ switch (headers.operation) {
41
+ case `insert`:
42
+ typeMap.set(key, value);
43
+ break;
44
+ case `update`:
45
+ typeMap.set(key, value);
46
+ break;
47
+ case `upsert`:
48
+ typeMap.set(key, value);
49
+ break;
50
+ case `delete`:
51
+ typeMap.delete(key);
52
+ break;
53
+ }
54
+ }
55
+ /**
56
+ * Apply a batch of change events
57
+ */
58
+ applyBatch(events) {
59
+ for (const event of events) this.apply(event);
60
+ }
61
+ /**
62
+ * Get a specific value by type and key
63
+ */
64
+ get(type, key) {
65
+ const typeMap = this.data.get(type);
66
+ if (!typeMap) return void 0;
67
+ return typeMap.get(key);
68
+ }
69
+ /**
70
+ * Get all entries for a specific type
71
+ */
72
+ getType(type) {
73
+ return this.data.get(type) || new Map();
74
+ }
75
+ /**
76
+ * Clear all state
77
+ */
78
+ clear() {
79
+ this.data.clear();
80
+ }
81
+ /**
82
+ * Get the number of types in the state
83
+ */
84
+ get typeCount() {
85
+ return this.data.size;
86
+ }
87
+ /**
88
+ * Get all type names
89
+ */
90
+ get types() {
91
+ return Array.from(this.data.keys());
92
+ }
93
+ };
94
+
95
+ //#endregion
96
+ //#region src/schema.ts
97
+ /**
98
+ * Reserved collection names that would collide with StreamDB properties
99
+ * (collections are now namespaced, but we still prevent internal name collisions)
100
+ */
101
+ const RESERVED_COLLECTION_NAMES = new Set([
102
+ `collections`,
103
+ `preload`,
104
+ `close`,
105
+ `utils`,
106
+ `actions`
107
+ ]);
108
+ /**
109
+ * Create helper functions for a collection
110
+ */
111
+ function createCollectionHelpers(eventType, primaryKey, schema) {
112
+ return {
113
+ insert: ({ key, value, headers }) => {
114
+ const result = schema[`~standard`].validate(value);
115
+ if (`issues` in result) throw new Error(`Validation failed for ${eventType} insert: ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`);
116
+ const derived = value[primaryKey];
117
+ const finalKey = key ?? (derived != null && derived !== `` ? String(derived) : void 0);
118
+ if (finalKey == null || finalKey === ``) throw new Error(`Cannot create ${eventType} insert event: must provide either 'key' or a value with a non-empty '${primaryKey}' field`);
119
+ return {
120
+ type: eventType,
121
+ key: finalKey,
122
+ value,
123
+ headers: {
124
+ ...headers,
125
+ operation: `insert`
126
+ }
127
+ };
128
+ },
129
+ update: ({ key, value, oldValue, headers }) => {
130
+ const result = schema[`~standard`].validate(value);
131
+ if (`issues` in result) throw new Error(`Validation failed for ${eventType} update: ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`);
132
+ if (oldValue !== void 0) {
133
+ const oldResult = schema[`~standard`].validate(oldValue);
134
+ if (`issues` in oldResult) throw new Error(`Validation failed for ${eventType} update (oldValue): ${oldResult.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`);
135
+ }
136
+ const derived = value[primaryKey];
137
+ const finalKey = key ?? (derived != null && derived !== `` ? String(derived) : void 0);
138
+ if (finalKey == null || finalKey === ``) throw new Error(`Cannot create ${eventType} update event: must provide either 'key' or a value with a non-empty '${primaryKey}' field`);
139
+ return {
140
+ type: eventType,
141
+ key: finalKey,
142
+ value,
143
+ old_value: oldValue,
144
+ headers: {
145
+ ...headers,
146
+ operation: `update`
147
+ }
148
+ };
149
+ },
150
+ delete: ({ key, oldValue, headers }) => {
151
+ if (oldValue !== void 0) {
152
+ const result = schema[`~standard`].validate(oldValue);
153
+ if (`issues` in result) throw new Error(`Validation failed for ${eventType} delete (oldValue): ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`);
154
+ }
155
+ const finalKey = key ?? (oldValue ? String(oldValue[primaryKey]) : void 0);
156
+ if (!finalKey) throw new Error(`Cannot create ${eventType} delete event: must provide either 'key' or 'oldValue' with a ${primaryKey} field`);
157
+ return {
158
+ type: eventType,
159
+ key: finalKey,
160
+ old_value: oldValue,
161
+ headers: {
162
+ ...headers,
163
+ operation: `delete`
164
+ }
165
+ };
166
+ },
167
+ upsert: ({ key, value, headers }) => {
168
+ const result = schema[`~standard`].validate(value);
169
+ if (`issues` in result) throw new Error(`Validation failed for ${eventType} upsert: ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`);
170
+ const derived = value[primaryKey];
171
+ const finalKey = key ?? (derived != null && derived !== `` ? String(derived) : void 0);
172
+ if (finalKey == null || finalKey === ``) throw new Error(`Cannot create ${eventType} upsert event: must provide either 'key' or a value with a non-empty '${primaryKey}' field`);
173
+ return {
174
+ type: eventType,
175
+ key: finalKey,
176
+ value,
177
+ headers: {
178
+ ...headers,
179
+ operation: `upsert`
180
+ }
181
+ };
182
+ }
183
+ };
184
+ }
185
+ /**
186
+ * Create a state schema definition with typed collections and event helpers
187
+ */
188
+ function createStateSchema(collections) {
189
+ for (const name of Object.keys(collections)) if (RESERVED_COLLECTION_NAMES.has(name)) throw new Error(`Reserved collection name "${name}" - this would collide with StreamDB properties (${Array.from(RESERVED_COLLECTION_NAMES).join(`, `)})`);
190
+ const typeToCollection = new Map();
191
+ for (const [collectionName, def] of Object.entries(collections)) {
192
+ const existing = typeToCollection.get(def.type);
193
+ if (existing) throw new Error(`Duplicate event type "${def.type}" - used by both "${existing}" and "${collectionName}" collections`);
194
+ typeToCollection.set(def.type, collectionName);
195
+ }
196
+ const enhancedCollections = {};
197
+ for (const [name, collectionDef] of Object.entries(collections)) enhancedCollections[name] = {
198
+ ...collectionDef,
199
+ ...createCollectionHelpers(collectionDef.type, collectionDef.primaryKey, collectionDef.schema)
200
+ };
201
+ return enhancedCollections;
202
+ }
203
+
204
+ //#endregion
205
+ Object.defineProperty(exports, 'MaterializedState', {
206
+ enumerable: true,
207
+ get: function () {
208
+ return MaterializedState;
209
+ }
210
+ });
211
+ Object.defineProperty(exports, 'createStateSchema', {
212
+ enumerable: true,
213
+ get: function () {
214
+ return createStateSchema;
215
+ }
216
+ });
217
+ Object.defineProperty(exports, 'isChangeEvent', {
218
+ enumerable: true,
219
+ get: function () {
220
+ return isChangeEvent;
221
+ }
222
+ });
223
+ Object.defineProperty(exports, 'isControlEvent', {
224
+ enumerable: true,
225
+ get: function () {
226
+ return isControlEvent;
227
+ }
228
+ });
@@ -0,0 +1,203 @@
1
+ //#region src/types.ts
2
+ /**
3
+ * Type guard to check if an event is a change event
4
+ */
5
+ function isChangeEvent(event) {
6
+ return event != null && `operation` in event.headers;
7
+ }
8
+ /**
9
+ * Type guard to check if an event is a control event
10
+ */
11
+ function isControlEvent(event) {
12
+ return event != null && `control` in event.headers;
13
+ }
14
+
15
+ //#endregion
16
+ //#region src/materialized-state.ts
17
+ /**
18
+ * MaterializedState maintains an in-memory view of state from change events.
19
+ *
20
+ * It organizes data by type, where each type contains a map of key -> value.
21
+ * This supports multi-type streams where different entity types can coexist.
22
+ */
23
+ var MaterializedState = class {
24
+ data;
25
+ constructor() {
26
+ this.data = new Map();
27
+ }
28
+ /**
29
+ * Apply a single change event to update the materialized state
30
+ */
31
+ apply(event) {
32
+ const { type, key, value, headers } = event;
33
+ let typeMap = this.data.get(type);
34
+ if (!typeMap) {
35
+ typeMap = new Map();
36
+ this.data.set(type, typeMap);
37
+ }
38
+ switch (headers.operation) {
39
+ case `insert`:
40
+ typeMap.set(key, value);
41
+ break;
42
+ case `update`:
43
+ typeMap.set(key, value);
44
+ break;
45
+ case `upsert`:
46
+ typeMap.set(key, value);
47
+ break;
48
+ case `delete`:
49
+ typeMap.delete(key);
50
+ break;
51
+ }
52
+ }
53
+ /**
54
+ * Apply a batch of change events
55
+ */
56
+ applyBatch(events) {
57
+ for (const event of events) this.apply(event);
58
+ }
59
+ /**
60
+ * Get a specific value by type and key
61
+ */
62
+ get(type, key) {
63
+ const typeMap = this.data.get(type);
64
+ if (!typeMap) return void 0;
65
+ return typeMap.get(key);
66
+ }
67
+ /**
68
+ * Get all entries for a specific type
69
+ */
70
+ getType(type) {
71
+ return this.data.get(type) || new Map();
72
+ }
73
+ /**
74
+ * Clear all state
75
+ */
76
+ clear() {
77
+ this.data.clear();
78
+ }
79
+ /**
80
+ * Get the number of types in the state
81
+ */
82
+ get typeCount() {
83
+ return this.data.size;
84
+ }
85
+ /**
86
+ * Get all type names
87
+ */
88
+ get types() {
89
+ return Array.from(this.data.keys());
90
+ }
91
+ };
92
+
93
+ //#endregion
94
+ //#region src/schema.ts
95
+ /**
96
+ * Reserved collection names that would collide with StreamDB properties
97
+ * (collections are now namespaced, but we still prevent internal name collisions)
98
+ */
99
+ const RESERVED_COLLECTION_NAMES = new Set([
100
+ `collections`,
101
+ `preload`,
102
+ `close`,
103
+ `utils`,
104
+ `actions`
105
+ ]);
106
+ /**
107
+ * Create helper functions for a collection
108
+ */
109
+ function createCollectionHelpers(eventType, primaryKey, schema) {
110
+ return {
111
+ insert: ({ key, value, headers }) => {
112
+ const result = schema[`~standard`].validate(value);
113
+ if (`issues` in result) throw new Error(`Validation failed for ${eventType} insert: ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`);
114
+ const derived = value[primaryKey];
115
+ const finalKey = key ?? (derived != null && derived !== `` ? String(derived) : void 0);
116
+ if (finalKey == null || finalKey === ``) throw new Error(`Cannot create ${eventType} insert event: must provide either 'key' or a value with a non-empty '${primaryKey}' field`);
117
+ return {
118
+ type: eventType,
119
+ key: finalKey,
120
+ value,
121
+ headers: {
122
+ ...headers,
123
+ operation: `insert`
124
+ }
125
+ };
126
+ },
127
+ update: ({ key, value, oldValue, headers }) => {
128
+ const result = schema[`~standard`].validate(value);
129
+ if (`issues` in result) throw new Error(`Validation failed for ${eventType} update: ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`);
130
+ if (oldValue !== void 0) {
131
+ const oldResult = schema[`~standard`].validate(oldValue);
132
+ if (`issues` in oldResult) throw new Error(`Validation failed for ${eventType} update (oldValue): ${oldResult.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`);
133
+ }
134
+ const derived = value[primaryKey];
135
+ const finalKey = key ?? (derived != null && derived !== `` ? String(derived) : void 0);
136
+ if (finalKey == null || finalKey === ``) throw new Error(`Cannot create ${eventType} update event: must provide either 'key' or a value with a non-empty '${primaryKey}' field`);
137
+ return {
138
+ type: eventType,
139
+ key: finalKey,
140
+ value,
141
+ old_value: oldValue,
142
+ headers: {
143
+ ...headers,
144
+ operation: `update`
145
+ }
146
+ };
147
+ },
148
+ delete: ({ key, oldValue, headers }) => {
149
+ if (oldValue !== void 0) {
150
+ const result = schema[`~standard`].validate(oldValue);
151
+ if (`issues` in result) throw new Error(`Validation failed for ${eventType} delete (oldValue): ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`);
152
+ }
153
+ const finalKey = key ?? (oldValue ? String(oldValue[primaryKey]) : void 0);
154
+ if (!finalKey) throw new Error(`Cannot create ${eventType} delete event: must provide either 'key' or 'oldValue' with a ${primaryKey} field`);
155
+ return {
156
+ type: eventType,
157
+ key: finalKey,
158
+ old_value: oldValue,
159
+ headers: {
160
+ ...headers,
161
+ operation: `delete`
162
+ }
163
+ };
164
+ },
165
+ upsert: ({ key, value, headers }) => {
166
+ const result = schema[`~standard`].validate(value);
167
+ if (`issues` in result) throw new Error(`Validation failed for ${eventType} upsert: ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`);
168
+ const derived = value[primaryKey];
169
+ const finalKey = key ?? (derived != null && derived !== `` ? String(derived) : void 0);
170
+ if (finalKey == null || finalKey === ``) throw new Error(`Cannot create ${eventType} upsert event: must provide either 'key' or a value with a non-empty '${primaryKey}' field`);
171
+ return {
172
+ type: eventType,
173
+ key: finalKey,
174
+ value,
175
+ headers: {
176
+ ...headers,
177
+ operation: `upsert`
178
+ }
179
+ };
180
+ }
181
+ };
182
+ }
183
+ /**
184
+ * Create a state schema definition with typed collections and event helpers
185
+ */
186
+ function createStateSchema(collections) {
187
+ for (const name of Object.keys(collections)) if (RESERVED_COLLECTION_NAMES.has(name)) throw new Error(`Reserved collection name "${name}" - this would collide with StreamDB properties (${Array.from(RESERVED_COLLECTION_NAMES).join(`, `)})`);
188
+ const typeToCollection = new Map();
189
+ for (const [collectionName, def] of Object.entries(collections)) {
190
+ const existing = typeToCollection.get(def.type);
191
+ if (existing) throw new Error(`Duplicate event type "${def.type}" - used by both "${existing}" and "${collectionName}" collections`);
192
+ typeToCollection.set(def.type, collectionName);
193
+ }
194
+ const enhancedCollections = {};
195
+ for (const [name, collectionDef] of Object.entries(collections)) enhancedCollections[name] = {
196
+ ...collectionDef,
197
+ ...createCollectionHelpers(collectionDef.type, collectionDef.primaryKey, collectionDef.schema)
198
+ };
199
+ return enhancedCollections;
200
+ }
201
+
202
+ //#endregion
203
+ export { MaterializedState, createStateSchema, isChangeEvent, isControlEvent };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@durable-streams/state",
3
3
  "description": "State change event protocol for Durable Streams",
4
- "version": "0.2.9",
4
+ "version": "0.3.1",
5
5
  "author": "Durable Stream contributors",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -33,6 +33,16 @@
33
33
  "default": "./dist/index.cjs"
34
34
  }
35
35
  },
36
+ "./db": {
37
+ "import": {
38
+ "types": "./dist/db.d.ts",
39
+ "default": "./dist/db.js"
40
+ },
41
+ "require": {
42
+ "types": "./dist/db.d.cts",
43
+ "default": "./dist/db.cjs"
44
+ }
45
+ },
36
46
  "./package.json": "./package.json"
37
47
  },
38
48
  "sideEffects": false,
@@ -50,13 +60,21 @@
50
60
  ],
51
61
  "dependencies": {
52
62
  "@standard-schema/spec": "^1.0.0",
53
- "@tanstack/db": "^0.6.0",
54
63
  "@durable-streams/client": "0.2.6"
55
64
  },
65
+ "peerDependencies": {
66
+ "@tanstack/db": ">=0.6.0 <1.0.0"
67
+ },
68
+ "peerDependenciesMeta": {
69
+ "@tanstack/db": {
70
+ "optional": true
71
+ }
72
+ },
56
73
  "devDependencies": {
74
+ "@tanstack/db": "0.6.0",
57
75
  "@tanstack/intent": "latest",
58
76
  "tsdown": "^0.9.0",
59
- "@durable-streams/server": "0.3.5"
77
+ "@durable-streams/server": "0.3.7"
60
78
  },
61
79
  "engines": {
62
80
  "node": ">=18.0.0"
@@ -28,7 +28,7 @@ optimistic actions, and transaction confirmation.
28
28
  ## Setup
29
29
 
30
30
  ```typescript
31
- import { createStreamDB, createStateSchema } from "@durable-streams/state"
31
+ import { createStreamDB, createStateSchema } from "@durable-streams/state/db"
32
32
  import { DurableStream } from "@durable-streams/client"
33
33
  import { z } from "zod"
34
34
 
@@ -83,7 +83,7 @@ StreamDB collections are TanStack DB collections. Use framework adapters for rea
83
83
 
84
84
  ```typescript
85
85
  import { useLiveQuery } from "@tanstack/react-db"
86
- import { eq } from "@durable-streams/state"
86
+ import { eq } from "@durable-streams/state/db"
87
87
 
88
88
  // List query — destructure { data } with a default empty array
89
89
  function UserList() {
@@ -111,7 +111,7 @@ function UserProfile({ userId }: { userId: string }) {
111
111
  ### Optimistic actions with server confirmation
112
112
 
113
113
  ```typescript
114
- import { createStreamDB, createStateSchema } from "@durable-streams/state"
114
+ import { createStreamDB, createStateSchema } from "@durable-streams/state/db"
115
115
  import { z } from "zod"
116
116
 
117
117
  const schema = createStateSchema({
package/src/db.ts ADDED
@@ -0,0 +1,61 @@
1
+ // `@durable-streams/state/db`: the full surface, including the reactive,
2
+ // TanStack DB-backed StreamDB layer. Importing this entry pulls in @tanstack/db,
3
+ // which is a peer dependency — consumers using this subpath must install it.
4
+ //
5
+ // It is a strict superset of the db-free main entry (`@durable-streams/state`),
6
+ // so code that uses both `createStateSchema` and `createStreamDB` can import
7
+ // everything from here.
8
+
9
+ export * from "./index"
10
+
11
+ // Reactive StreamDB layer
12
+ export { createStreamDB, getStreamDBCollectionId } from "./stream-db"
13
+ export type {
14
+ CreateStreamDBOptions,
15
+ StreamDB,
16
+ StreamDBMethods,
17
+ StreamDBUtils,
18
+ StreamDBWithActions,
19
+ ActionFactory,
20
+ ActionMap,
21
+ ActionDefinition,
22
+ } from "./stream-db"
23
+
24
+ // Re-export key types and utilities from @tanstack/db for convenience.
25
+ // This ensures consumers can use the same module resolution for type compatibility.
26
+ export type { Collection, SyncConfig } from "@tanstack/db"
27
+ export {
28
+ createCollection,
29
+ createLiveQueryCollection,
30
+ createOptimisticAction,
31
+ createTransaction,
32
+ deepEquals,
33
+ localOnlyCollectionOptions,
34
+ queryOnce,
35
+ // Comparison operators
36
+ eq,
37
+ gt,
38
+ gte,
39
+ lt,
40
+ lte,
41
+ like,
42
+ ilike,
43
+ inArray,
44
+ // Logical operators
45
+ and,
46
+ or,
47
+ not,
48
+ // Null checking
49
+ isNull,
50
+ isUndefined,
51
+ // Aggregate functions
52
+ count,
53
+ sum,
54
+ avg,
55
+ min,
56
+ max,
57
+ // Includes/projection functions
58
+ concat,
59
+ coalesce,
60
+ toArray,
61
+ } from "@tanstack/db"
package/src/index.ts CHANGED
@@ -1,3 +1,11 @@
1
+ // Main entry: the db-free surface of the state protocol.
2
+ //
3
+ // Everything exported here works without @tanstack/db installed — defining
4
+ // schemas, constructing/validating change events, and materializing state
5
+ // in-memory. The reactive, TanStack DB-backed StreamDB layer (createStreamDB,
6
+ // live queries, optimistic actions) lives under the `@durable-streams/state/db`
7
+ // subpath, which carries the `@tanstack/db` peer dependency.
8
+
1
9
  // Types
2
10
  export type {
3
11
  Operation,
@@ -11,66 +19,15 @@ export type {
11
19
 
12
20
  export { isChangeEvent, isControlEvent } from "./types"
13
21
 
14
- // Classes
22
+ // In-memory materialization
15
23
  export { MaterializedState } from "./materialized-state"
16
24
 
17
- // Stream DB
18
- export {
19
- createStreamDB,
20
- createStateSchema,
21
- getStreamDBCollectionId,
22
- } from "./stream-db"
25
+ // Schema definition + event construction (producer side)
26
+ export { createStateSchema } from "./schema"
23
27
  export type {
24
28
  CollectionDefinition,
25
29
  CollectionEventHelpers,
26
30
  CollectionWithHelpers,
27
31
  StreamStateDefinition,
28
32
  StateSchema,
29
- CreateStreamDBOptions,
30
- StreamDB,
31
- StreamDBMethods,
32
- StreamDBUtils,
33
- StreamDBWithActions,
34
- ActionFactory,
35
- ActionMap,
36
- ActionDefinition,
37
- } from "./stream-db"
38
-
39
- // Re-export key types and utilities from @tanstack/db for convenience
40
- // This ensures consumers can use the same module resolution for type compatibility
41
- export type { Collection, SyncConfig } from "@tanstack/db"
42
- export {
43
- createCollection,
44
- createLiveQueryCollection,
45
- createOptimisticAction,
46
- createTransaction,
47
- deepEquals,
48
- localOnlyCollectionOptions,
49
- queryOnce,
50
- // Comparison operators
51
- eq,
52
- gt,
53
- gte,
54
- lt,
55
- lte,
56
- like,
57
- ilike,
58
- inArray,
59
- // Logical operators
60
- and,
61
- or,
62
- not,
63
- // Null checking
64
- isNull,
65
- isUndefined,
66
- // Aggregate functions
67
- count,
68
- sum,
69
- avg,
70
- min,
71
- max,
72
- // Includes/projection functions
73
- concat,
74
- coalesce,
75
- toArray,
76
- } from "@tanstack/db"
33
+ } from "./schema"