@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.
package/src/schema.ts ADDED
@@ -0,0 +1,265 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec"
2
+ import type { ChangeEvent } from "./types"
3
+
4
+ // ============================================================================
5
+ // Schema Definitions
6
+ //
7
+ // This module is intentionally free of any @tanstack/db dependency: it covers
8
+ // the producer side of the state protocol (defining schemas and constructing
9
+ // validated change events). The reactive, TanStack DB-backed surface lives in
10
+ // ./stream-db and is published under the `@durable-streams/state/db` subpath.
11
+ // ============================================================================
12
+
13
+ /**
14
+ * Definition for a single collection in the stream state
15
+ */
16
+ export interface CollectionDefinition<T = unknown> {
17
+ /** Standard Schema for validating values */
18
+ schema: StandardSchemaV1<T>
19
+ /** The type field value in change events that map to this collection */
20
+ type: string
21
+ /** The property name in T that serves as the primary key */
22
+ primaryKey: string
23
+ }
24
+
25
+ /**
26
+ * Helper methods for creating change events for a collection
27
+ */
28
+ export interface CollectionEventHelpers<T> {
29
+ /**
30
+ * Create an insert change event
31
+ */
32
+ insert: (params: {
33
+ key?: string
34
+ value: T
35
+ headers?: Omit<Record<string, string>, `operation`>
36
+ }) => ChangeEvent<T>
37
+ /**
38
+ * Create an update change event
39
+ */
40
+ update: (params: {
41
+ key?: string
42
+ value: T
43
+ oldValue?: T
44
+ headers?: Omit<Record<string, string>, `operation`>
45
+ }) => ChangeEvent<T>
46
+ /**
47
+ * Create a delete change event
48
+ */
49
+ delete: (params: {
50
+ key?: string
51
+ oldValue?: T
52
+ headers?: Omit<Record<string, string>, `operation`>
53
+ }) => ChangeEvent<T>
54
+ /**
55
+ * Create an upsert change event (insert or update)
56
+ */
57
+ upsert: (params: {
58
+ key?: string
59
+ value: T
60
+ headers?: Omit<Record<string, string>, `operation`>
61
+ }) => ChangeEvent<T>
62
+ }
63
+
64
+ /**
65
+ * Collection definition enhanced with event creation helpers
66
+ */
67
+ export type CollectionWithHelpers<T = unknown> = CollectionDefinition<T> &
68
+ CollectionEventHelpers<T>
69
+
70
+ /**
71
+ * Stream state definition containing all collections
72
+ */
73
+ export type StreamStateDefinition = Record<string, CollectionDefinition>
74
+
75
+ /**
76
+ * Stream state schema with helper methods for creating change events
77
+ */
78
+ export type StateSchema<T extends Record<string, CollectionDefinition>> = {
79
+ [K in keyof T]: CollectionWithHelpers<
80
+ T[K] extends CollectionDefinition<infer U> ? U : unknown
81
+ >
82
+ }
83
+
84
+ /**
85
+ * Reserved collection names that would collide with StreamDB properties
86
+ * (collections are now namespaced, but we still prevent internal name collisions)
87
+ */
88
+ const RESERVED_COLLECTION_NAMES = new Set([
89
+ `collections`,
90
+ `preload`,
91
+ `close`,
92
+ `utils`,
93
+ `actions`,
94
+ ])
95
+
96
+ /**
97
+ * Create helper functions for a collection
98
+ */
99
+ function createCollectionHelpers<T>(
100
+ eventType: string,
101
+ primaryKey: string,
102
+ schema: StandardSchemaV1<T>
103
+ ): CollectionEventHelpers<T> {
104
+ return {
105
+ insert: ({ key, value, headers }): ChangeEvent<T> => {
106
+ // Validate value
107
+ const result = schema[`~standard`].validate(value)
108
+ if (`issues` in result) {
109
+ throw new Error(
110
+ `Validation failed for ${eventType} insert: ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
111
+ )
112
+ }
113
+
114
+ // Derive key from value if not explicitly provided
115
+ const derived = (value as any)[primaryKey]
116
+ const finalKey =
117
+ key ?? (derived != null && derived !== `` ? String(derived) : undefined)
118
+ if (finalKey == null || finalKey === ``) {
119
+ throw new Error(
120
+ `Cannot create ${eventType} insert event: must provide either 'key' or a value with a non-empty '${primaryKey}' field`
121
+ )
122
+ }
123
+
124
+ return {
125
+ type: eventType,
126
+ key: finalKey,
127
+ value,
128
+ headers: { ...headers, operation: `insert` },
129
+ }
130
+ },
131
+ update: ({ key, value, oldValue, headers }): ChangeEvent<T> => {
132
+ // Validate value
133
+ const result = schema[`~standard`].validate(value)
134
+ if (`issues` in result) {
135
+ throw new Error(
136
+ `Validation failed for ${eventType} update: ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
137
+ )
138
+ }
139
+
140
+ // Optionally validate oldValue if provided
141
+ if (oldValue !== undefined) {
142
+ const oldResult = schema[`~standard`].validate(oldValue)
143
+ if (`issues` in oldResult) {
144
+ throw new Error(
145
+ `Validation failed for ${eventType} update (oldValue): ${oldResult.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
146
+ )
147
+ }
148
+ }
149
+
150
+ // Derive key from value if not explicitly provided
151
+ const derived = (value as any)[primaryKey]
152
+ const finalKey =
153
+ key ?? (derived != null && derived !== `` ? String(derived) : undefined)
154
+ if (finalKey == null || finalKey === ``) {
155
+ throw new Error(
156
+ `Cannot create ${eventType} update event: must provide either 'key' or a value with a non-empty '${primaryKey}' field`
157
+ )
158
+ }
159
+
160
+ return {
161
+ type: eventType,
162
+ key: finalKey,
163
+ value,
164
+ old_value: oldValue,
165
+ headers: { ...headers, operation: `update` },
166
+ }
167
+ },
168
+ delete: ({ key, oldValue, headers }): ChangeEvent<T> => {
169
+ // Optionally validate oldValue if provided
170
+ if (oldValue !== undefined) {
171
+ const result = schema[`~standard`].validate(oldValue)
172
+ if (`issues` in result) {
173
+ throw new Error(
174
+ `Validation failed for ${eventType} delete (oldValue): ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
175
+ )
176
+ }
177
+ }
178
+
179
+ // Ensure we have either key or oldValue to derive the key from
180
+ const finalKey =
181
+ key ?? (oldValue ? String((oldValue as any)[primaryKey]) : undefined)
182
+ if (!finalKey) {
183
+ throw new Error(
184
+ `Cannot create ${eventType} delete event: must provide either 'key' or 'oldValue' with a ${primaryKey} field`
185
+ )
186
+ }
187
+
188
+ return {
189
+ type: eventType,
190
+ key: finalKey,
191
+ old_value: oldValue,
192
+ headers: { ...headers, operation: `delete` },
193
+ }
194
+ },
195
+ upsert: ({ key, value, headers }): ChangeEvent<T> => {
196
+ // Validate value
197
+ const result = schema[`~standard`].validate(value)
198
+ if (`issues` in result) {
199
+ throw new Error(
200
+ `Validation failed for ${eventType} upsert: ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
201
+ )
202
+ }
203
+
204
+ // Derive key from value if not explicitly provided
205
+ const derived = (value as any)[primaryKey]
206
+ const finalKey =
207
+ key ?? (derived != null && derived !== `` ? String(derived) : undefined)
208
+ if (finalKey == null || finalKey === ``) {
209
+ throw new Error(
210
+ `Cannot create ${eventType} upsert event: must provide either 'key' or a value with a non-empty '${primaryKey}' field`
211
+ )
212
+ }
213
+
214
+ return {
215
+ type: eventType,
216
+ key: finalKey,
217
+ value,
218
+ headers: { ...headers, operation: `upsert` },
219
+ }
220
+ },
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Create a state schema definition with typed collections and event helpers
226
+ */
227
+ export function createStateSchema<
228
+ T extends Record<string, CollectionDefinition>,
229
+ >(collections: T): StateSchema<T> {
230
+ // Validate no reserved collection names
231
+ for (const name of Object.keys(collections)) {
232
+ if (RESERVED_COLLECTION_NAMES.has(name)) {
233
+ throw new Error(
234
+ `Reserved collection name "${name}" - this would collide with StreamDB properties (${Array.from(RESERVED_COLLECTION_NAMES).join(`, `)})`
235
+ )
236
+ }
237
+ }
238
+
239
+ // Validate no duplicate event types
240
+ const typeToCollection = new Map<string, string>()
241
+ for (const [collectionName, def] of Object.entries(collections)) {
242
+ const existing = typeToCollection.get(def.type)
243
+ if (existing) {
244
+ throw new Error(
245
+ `Duplicate event type "${def.type}" - used by both "${existing}" and "${collectionName}" collections`
246
+ )
247
+ }
248
+ typeToCollection.set(def.type, collectionName)
249
+ }
250
+
251
+ // Enhance collections with helper methods
252
+ const enhancedCollections: any = {}
253
+ for (const [name, collectionDef] of Object.entries(collections)) {
254
+ enhancedCollections[name] = {
255
+ ...collectionDef,
256
+ ...createCollectionHelpers(
257
+ collectionDef.type,
258
+ collectionDef.primaryKey,
259
+ collectionDef.schema
260
+ ),
261
+ }
262
+ }
263
+
264
+ return enhancedCollections
265
+ }
package/src/stream-db.ts CHANGED
@@ -15,82 +15,24 @@ import type {
15
15
  LiveMode,
16
16
  StreamResponse,
17
17
  } from "@durable-streams/client"
18
+ import type { CollectionDefinition, StreamStateDefinition } from "./schema"
19
+
20
+ // Schema definitions and event construction are db-free and live in ./schema.
21
+ // Re-export them here so the TanStack-backed `@durable-streams/state/db`
22
+ // surface stays a superset of the db-free main entry.
23
+ export { createStateSchema } from "./schema"
24
+ export type {
25
+ CollectionDefinition,
26
+ CollectionEventHelpers,
27
+ CollectionWithHelpers,
28
+ StreamStateDefinition,
29
+ StateSchema,
30
+ } from "./schema"
18
31
 
19
32
  // ============================================================================
20
33
  // Type Definitions
21
34
  // ============================================================================
22
35
 
23
- /**
24
- * Definition for a single collection in the stream state
25
- */
26
- export interface CollectionDefinition<T = unknown> {
27
- /** Standard Schema for validating values */
28
- schema: StandardSchemaV1<T>
29
- /** The type field value in change events that map to this collection */
30
- type: string
31
- /** The property name in T that serves as the primary key */
32
- primaryKey: string
33
- }
34
-
35
- /**
36
- * Helper methods for creating change events for a collection
37
- */
38
- export interface CollectionEventHelpers<T> {
39
- /**
40
- * Create an insert change event
41
- */
42
- insert: (params: {
43
- key?: string
44
- value: T
45
- headers?: Omit<Record<string, string>, `operation`>
46
- }) => ChangeEvent<T>
47
- /**
48
- * Create an update change event
49
- */
50
- update: (params: {
51
- key?: string
52
- value: T
53
- oldValue?: T
54
- headers?: Omit<Record<string, string>, `operation`>
55
- }) => ChangeEvent<T>
56
- /**
57
- * Create a delete change event
58
- */
59
- delete: (params: {
60
- key?: string
61
- oldValue?: T
62
- headers?: Omit<Record<string, string>, `operation`>
63
- }) => ChangeEvent<T>
64
- /**
65
- * Create an upsert change event (insert or update)
66
- */
67
- upsert: (params: {
68
- key?: string
69
- value: T
70
- headers?: Omit<Record<string, string>, `operation`>
71
- }) => ChangeEvent<T>
72
- }
73
-
74
- /**
75
- * Collection definition enhanced with event creation helpers
76
- */
77
- export type CollectionWithHelpers<T = unknown> = CollectionDefinition<T> &
78
- CollectionEventHelpers<T>
79
-
80
- /**
81
- * Stream state definition containing all collections
82
- */
83
- export type StreamStateDefinition = Record<string, CollectionDefinition>
84
-
85
- /**
86
- * Stream state schema with helper methods for creating change events
87
- */
88
- export type StateSchema<T extends Record<string, CollectionDefinition>> = {
89
- [K in keyof T]: CollectionWithHelpers<
90
- T[K] extends CollectionDefinition<infer U> ? U : unknown
91
- >
92
- }
93
-
94
36
  /**
95
37
  * Definition for a single action that can be passed to createOptimisticAction
96
38
  */
@@ -619,189 +561,6 @@ function createStreamSyncConfig<T extends object>(
619
561
  // Main Implementation
620
562
  // ============================================================================
621
563
 
622
- /**
623
- * Reserved collection names that would collide with StreamDB properties
624
- * (collections are now namespaced, but we still prevent internal name collisions)
625
- */
626
- const RESERVED_COLLECTION_NAMES = new Set([
627
- `collections`,
628
- `preload`,
629
- `close`,
630
- `utils`,
631
- `actions`,
632
- ])
633
-
634
- /**
635
- * Create helper functions for a collection
636
- */
637
- function createCollectionHelpers<T>(
638
- eventType: string,
639
- primaryKey: string,
640
- schema: StandardSchemaV1<T>
641
- ): CollectionEventHelpers<T> {
642
- return {
643
- insert: ({ key, value, headers }): ChangeEvent<T> => {
644
- // Validate value
645
- const result = schema[`~standard`].validate(value)
646
- if (`issues` in result) {
647
- throw new Error(
648
- `Validation failed for ${eventType} insert: ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
649
- )
650
- }
651
-
652
- // Derive key from value if not explicitly provided
653
- const derived = (value as any)[primaryKey]
654
- const finalKey =
655
- key ?? (derived != null && derived !== `` ? String(derived) : undefined)
656
- if (finalKey == null || finalKey === ``) {
657
- throw new Error(
658
- `Cannot create ${eventType} insert event: must provide either 'key' or a value with a non-empty '${primaryKey}' field`
659
- )
660
- }
661
-
662
- return {
663
- type: eventType,
664
- key: finalKey,
665
- value,
666
- headers: { ...headers, operation: `insert` },
667
- }
668
- },
669
- update: ({ key, value, oldValue, headers }): ChangeEvent<T> => {
670
- // Validate value
671
- const result = schema[`~standard`].validate(value)
672
- if (`issues` in result) {
673
- throw new Error(
674
- `Validation failed for ${eventType} update: ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
675
- )
676
- }
677
-
678
- // Optionally validate oldValue if provided
679
- if (oldValue !== undefined) {
680
- const oldResult = schema[`~standard`].validate(oldValue)
681
- if (`issues` in oldResult) {
682
- throw new Error(
683
- `Validation failed for ${eventType} update (oldValue): ${oldResult.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
684
- )
685
- }
686
- }
687
-
688
- // Derive key from value if not explicitly provided
689
- const derived = (value as any)[primaryKey]
690
- const finalKey =
691
- key ?? (derived != null && derived !== `` ? String(derived) : undefined)
692
- if (finalKey == null || finalKey === ``) {
693
- throw new Error(
694
- `Cannot create ${eventType} update event: must provide either 'key' or a value with a non-empty '${primaryKey}' field`
695
- )
696
- }
697
-
698
- return {
699
- type: eventType,
700
- key: finalKey,
701
- value,
702
- old_value: oldValue,
703
- headers: { ...headers, operation: `update` },
704
- }
705
- },
706
- delete: ({ key, oldValue, headers }): ChangeEvent<T> => {
707
- // Optionally validate oldValue if provided
708
- if (oldValue !== undefined) {
709
- const result = schema[`~standard`].validate(oldValue)
710
- if (`issues` in result) {
711
- throw new Error(
712
- `Validation failed for ${eventType} delete (oldValue): ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
713
- )
714
- }
715
- }
716
-
717
- // Ensure we have either key or oldValue to derive the key from
718
- const finalKey =
719
- key ?? (oldValue ? String((oldValue as any)[primaryKey]) : undefined)
720
- if (!finalKey) {
721
- throw new Error(
722
- `Cannot create ${eventType} delete event: must provide either 'key' or 'oldValue' with a ${primaryKey} field`
723
- )
724
- }
725
-
726
- return {
727
- type: eventType,
728
- key: finalKey,
729
- old_value: oldValue,
730
- headers: { ...headers, operation: `delete` },
731
- }
732
- },
733
- upsert: ({ key, value, headers }): ChangeEvent<T> => {
734
- // Validate value
735
- const result = schema[`~standard`].validate(value)
736
- if (`issues` in result) {
737
- throw new Error(
738
- `Validation failed for ${eventType} upsert: ${result.issues?.map((i) => i.message).join(`, `) ?? `Unknown validation error`}`
739
- )
740
- }
741
-
742
- // Derive key from value if not explicitly provided
743
- const derived = (value as any)[primaryKey]
744
- const finalKey =
745
- key ?? (derived != null && derived !== `` ? String(derived) : undefined)
746
- if (finalKey == null || finalKey === ``) {
747
- throw new Error(
748
- `Cannot create ${eventType} upsert event: must provide either 'key' or a value with a non-empty '${primaryKey}' field`
749
- )
750
- }
751
-
752
- return {
753
- type: eventType,
754
- key: finalKey,
755
- value,
756
- headers: { ...headers, operation: `upsert` },
757
- }
758
- },
759
- }
760
- }
761
-
762
- /**
763
- * Create a state schema definition with typed collections and event helpers
764
- */
765
- export function createStateSchema<
766
- T extends Record<string, CollectionDefinition>,
767
- >(collections: T): StateSchema<T> {
768
- // Validate no reserved collection names
769
- for (const name of Object.keys(collections)) {
770
- if (RESERVED_COLLECTION_NAMES.has(name)) {
771
- throw new Error(
772
- `Reserved collection name "${name}" - this would collide with StreamDB properties (${Array.from(RESERVED_COLLECTION_NAMES).join(`, `)})`
773
- )
774
- }
775
- }
776
-
777
- // Validate no duplicate event types
778
- const typeToCollection = new Map<string, string>()
779
- for (const [collectionName, def] of Object.entries(collections)) {
780
- const existing = typeToCollection.get(def.type)
781
- if (existing) {
782
- throw new Error(
783
- `Duplicate event type "${def.type}" - used by both "${existing}" and "${collectionName}" collections`
784
- )
785
- }
786
- typeToCollection.set(def.type, collectionName)
787
- }
788
-
789
- // Enhance collections with helper methods
790
- const enhancedCollections: any = {}
791
- for (const [name, collectionDef] of Object.entries(collections)) {
792
- enhancedCollections[name] = {
793
- ...collectionDef,
794
- ...createCollectionHelpers(
795
- collectionDef.type,
796
- collectionDef.primaryKey,
797
- collectionDef.schema
798
- ),
799
- }
800
- }
801
-
802
- return enhancedCollections
803
- }
804
-
805
564
  /**
806
565
  * Create a stream-backed database with TanStack DB collections
807
566
  *