@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/README.md +12 -2
- package/dist/db.cjs +561 -0
- package/dist/db.d.cts +152 -0
- package/dist/db.d.ts +152 -0
- package/dist/db.js +364 -0
- package/dist/index-CqdIsdQy.d.cts +173 -0
- package/dist/index-D6Nak3Wl.d.ts +173 -0
- package/dist/index.cjs +5 -758
- package/dist/index.d.cts +2 -315
- package/dist/index.d.ts +2 -315
- package/dist/index.js +2 -561
- package/dist/src-AIE5IYwJ.cjs +228 -0
- package/dist/src-VTyL9Eij.js +203 -0
- package/package.json +21 -3
- package/skills/stream-db/SKILL.md +3 -3
- package/src/db.ts +61 -0
- package/src/index.ts +12 -55
- package/src/schema.ts +265 -0
- package/src/stream-db.ts +13 -254
package/dist/db.d.cts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { ChangeEvent, ChangeHeaders, CollectionDefinition, CollectionEventHelpers, CollectionWithHelpers, ControlEvent, MaterializedState, Operation, Row, StateEvent, StateSchema, StreamStateDefinition, Value, createStateSchema, isChangeEvent, isControlEvent } from "./index-CqdIsdQy.cjs";
|
|
2
|
+
import { Collection, Collection as Collection$1, SyncConfig, and, avg, coalesce, concat, count, createCollection, createLiveQueryCollection, createOptimisticAction, createOptimisticAction as createOptimisticAction$1, createTransaction, deepEquals, eq, gt, gte, ilike, inArray, isNull, isUndefined, like, localOnlyCollectionOptions, lt, lte, max, min, not, or, queryOnce, sum, toArray } from "@tanstack/db";
|
|
3
|
+
import { DurableStream, DurableStreamOptions, JsonBatch, LiveMode } from "@durable-streams/client";
|
|
4
|
+
|
|
5
|
+
//#region src/stream-db.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Definition for a single action that can be passed to createOptimisticAction
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Definition for a single action that can be passed to createOptimisticAction
|
|
12
|
+
*/
|
|
13
|
+
interface ActionDefinition<TParams = any, TContext = any> {
|
|
14
|
+
onMutate: (params: TParams) => void;
|
|
15
|
+
mutationFn: (params: TParams, context: TContext) => Promise<any>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Factory function for creating actions with access to db and stream context
|
|
19
|
+
*/
|
|
20
|
+
type ActionFactory<TDef extends StreamStateDefinition, TActions extends Record<string, ActionDefinition<any>>> = (context: {
|
|
21
|
+
db: StreamDB<TDef>;
|
|
22
|
+
stream: DurableStream;
|
|
23
|
+
}) => TActions;
|
|
24
|
+
/**
|
|
25
|
+
* Map action definitions to callable action functions
|
|
26
|
+
*/
|
|
27
|
+
type ActionMap<TActions extends Record<string, ActionDefinition<any>>> = { [K in keyof TActions]: ReturnType<typeof createOptimisticAction$1<any>> };
|
|
28
|
+
/**
|
|
29
|
+
* Options for creating a stream DB
|
|
30
|
+
*/
|
|
31
|
+
interface CreateStreamDBOptions<TDef extends StreamStateDefinition = StreamStateDefinition, TActions extends Record<string, ActionDefinition<any>> = Record<string, never>> {
|
|
32
|
+
/** Options for creating a new durable stream. Ignored when `stream` is provided. */
|
|
33
|
+
streamOptions?: DurableStreamOptions;
|
|
34
|
+
/** Pre-existing DurableStream instance to reuse (avoids creating a second connection). */
|
|
35
|
+
stream?: DurableStream;
|
|
36
|
+
/** Live read mode used by the StreamDB consumer. Defaults to true. */
|
|
37
|
+
live?: LiveMode;
|
|
38
|
+
/** The stream state definition */
|
|
39
|
+
state: TDef;
|
|
40
|
+
/** Optional factory function to create actions with db and stream context */
|
|
41
|
+
actions?: ActionFactory<TDef, TActions>;
|
|
42
|
+
/** Called for every ChangeEvent as it flows through the stream consumer. */
|
|
43
|
+
onEvent?: (event: ChangeEvent) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Called once per consumed stream batch before items are dispatched.
|
|
46
|
+
* Useful when external consumers need batch metadata available during
|
|
47
|
+
* downstream collection/effect processing.
|
|
48
|
+
*/
|
|
49
|
+
onBeforeBatch?: (batch: JsonBatch<StateEvent>) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Called once per consumed stream batch after items have been dispatched.
|
|
52
|
+
* Useful for tracking safe offsets for external ack/lease protocols.
|
|
53
|
+
*/
|
|
54
|
+
onBatch?: (batch: JsonBatch<StateEvent>) => void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Extract the value type from a CollectionDefinition
|
|
58
|
+
*/
|
|
59
|
+
type ExtractCollectionType<T extends CollectionDefinition> = T extends CollectionDefinition<infer U> ? U : unknown;
|
|
60
|
+
/**
|
|
61
|
+
* Map collection definitions to TanStack DB Collection types
|
|
62
|
+
*/
|
|
63
|
+
type CollectionMap<TDef extends StreamStateDefinition> = { [K in keyof TDef]: Collection$1<ExtractCollectionType<TDef[K]> & object, string> };
|
|
64
|
+
/**
|
|
65
|
+
* The StreamDB interface - provides typed access to collections
|
|
66
|
+
*/
|
|
67
|
+
type StreamDB<TDef extends StreamStateDefinition> = {
|
|
68
|
+
collections: CollectionMap<TDef>;
|
|
69
|
+
} & StreamDBMethods;
|
|
70
|
+
/**
|
|
71
|
+
* StreamDB with actions
|
|
72
|
+
*/
|
|
73
|
+
type StreamDBWithActions<TDef extends StreamStateDefinition, TActions extends Record<string, ActionDefinition<any>>> = StreamDB<TDef> & {
|
|
74
|
+
actions: ActionMap<TActions>;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Utility methods available on StreamDB
|
|
78
|
+
*/
|
|
79
|
+
interface StreamDBUtils {
|
|
80
|
+
/**
|
|
81
|
+
* Wait for a specific transaction ID to be synced through the stream
|
|
82
|
+
* @param txid The transaction ID to wait for (UUID string)
|
|
83
|
+
* @param timeout Optional timeout in milliseconds (defaults to 5000ms)
|
|
84
|
+
* @returns Promise that resolves when the txid is synced
|
|
85
|
+
*/
|
|
86
|
+
awaitTxId: (txid: string, timeout?: number) => Promise<void>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Methods available on a StreamDB instance
|
|
90
|
+
*/
|
|
91
|
+
interface StreamDBMethods {
|
|
92
|
+
/**
|
|
93
|
+
* The underlying DurableStream instance
|
|
94
|
+
*/
|
|
95
|
+
stream: DurableStream;
|
|
96
|
+
/**
|
|
97
|
+
* Current stream offset (tracks the last consumed position).
|
|
98
|
+
*/
|
|
99
|
+
readonly offset: string;
|
|
100
|
+
/**
|
|
101
|
+
* Preload all collections by consuming the stream until up-to-date
|
|
102
|
+
*/
|
|
103
|
+
preload: () => Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* Close the stream connection and cleanup
|
|
106
|
+
*/
|
|
107
|
+
close: () => void;
|
|
108
|
+
/**
|
|
109
|
+
* Utility methods for advanced stream operations
|
|
110
|
+
*/
|
|
111
|
+
utils: StreamDBUtils;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Build a TanStack collection id for a StreamDB collection.
|
|
115
|
+
*
|
|
116
|
+
* Collection ids must be unique per source stream, not just per schema key,
|
|
117
|
+
* otherwise joining the same collection name from two different streams can
|
|
118
|
+
* collapse to one logical source inside TanStack DB.
|
|
119
|
+
*/
|
|
120
|
+
declare function getStreamDBCollectionId(streamUrl: string, collectionName: string): string;
|
|
121
|
+
/**
|
|
122
|
+
* Create a stream-backed database with TanStack DB collections
|
|
123
|
+
*
|
|
124
|
+
* This function is synchronous - it creates the stream handle and collections
|
|
125
|
+
* but does not start the stream connection. Call `db.preload()` to connect
|
|
126
|
+
* and sync initial data.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* const stateSchema = createStateSchema({
|
|
131
|
+
* users: { schema: userSchema, type: "user", primaryKey: "id" },
|
|
132
|
+
* messages: { schema: messageSchema, type: "message", primaryKey: "id" },
|
|
133
|
+
* })
|
|
134
|
+
*
|
|
135
|
+
* // Create a stream DB (synchronous - stream is created lazily on preload)
|
|
136
|
+
* const db = createStreamDB({
|
|
137
|
+
* streamOptions: {
|
|
138
|
+
* url: "https://api.example.com/streams/my-stream",
|
|
139
|
+
* contentType: "application/json",
|
|
140
|
+
* },
|
|
141
|
+
* state: stateSchema,
|
|
142
|
+
* })
|
|
143
|
+
*
|
|
144
|
+
* // preload() creates the stream and loads initial data
|
|
145
|
+
* await db.preload()
|
|
146
|
+
* const user = await db.collections.users.get("123")
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
declare function createStreamDB<TDef extends StreamStateDefinition, TActions extends Record<string, ActionDefinition<any>> = Record<string, never>>(options: CreateStreamDBOptions<TDef, TActions>): TActions extends Record<string, never> ? StreamDB<TDef> : StreamDBWithActions<TDef, TActions>;
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
export { ActionDefinition, ActionFactory, ActionMap, ChangeEvent, ChangeHeaders, Collection, CollectionDefinition, CollectionEventHelpers, CollectionWithHelpers, ControlEvent, CreateStreamDBOptions, MaterializedState, Operation, Row, StateEvent, StateSchema, StreamDB, StreamDBMethods, StreamDBUtils, StreamDBWithActions, StreamStateDefinition, SyncConfig, Value, and, avg, coalesce, concat, count, createCollection, createLiveQueryCollection, createOptimisticAction, createStateSchema, createStreamDB, createTransaction, deepEquals, eq, getStreamDBCollectionId, gt, gte, ilike, inArray, isChangeEvent, isControlEvent, isNull, isUndefined, like, localOnlyCollectionOptions, lt, lte, max, min, not, or, queryOnce, sum, toArray };
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { ChangeEvent, ChangeHeaders, CollectionDefinition, CollectionEventHelpers, CollectionWithHelpers, ControlEvent, MaterializedState$1 as MaterializedState, Operation, Row, StateEvent, StateSchema, StreamStateDefinition, Value, createStateSchema$1 as createStateSchema, isChangeEvent$1 as isChangeEvent, isControlEvent$1 as isControlEvent } from "./index-D6Nak3Wl.js";
|
|
2
|
+
import { Collection, Collection as Collection$1, SyncConfig, and, avg, coalesce, concat, count, createCollection, createLiveQueryCollection, createOptimisticAction, createOptimisticAction as createOptimisticAction$1, createTransaction, deepEquals, eq, gt, gte, ilike, inArray, isNull, isUndefined, like, localOnlyCollectionOptions, lt, lte, max, min, not, or, queryOnce, sum, toArray } from "@tanstack/db";
|
|
3
|
+
import { DurableStream, DurableStreamOptions, JsonBatch, LiveMode } from "@durable-streams/client";
|
|
4
|
+
|
|
5
|
+
//#region src/stream-db.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Definition for a single action that can be passed to createOptimisticAction
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Definition for a single action that can be passed to createOptimisticAction
|
|
12
|
+
*/
|
|
13
|
+
interface ActionDefinition<TParams = any, TContext = any> {
|
|
14
|
+
onMutate: (params: TParams) => void;
|
|
15
|
+
mutationFn: (params: TParams, context: TContext) => Promise<any>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Factory function for creating actions with access to db and stream context
|
|
19
|
+
*/
|
|
20
|
+
type ActionFactory<TDef extends StreamStateDefinition, TActions extends Record<string, ActionDefinition<any>>> = (context: {
|
|
21
|
+
db: StreamDB<TDef>;
|
|
22
|
+
stream: DurableStream;
|
|
23
|
+
}) => TActions;
|
|
24
|
+
/**
|
|
25
|
+
* Map action definitions to callable action functions
|
|
26
|
+
*/
|
|
27
|
+
type ActionMap<TActions extends Record<string, ActionDefinition<any>>> = { [K in keyof TActions]: ReturnType<typeof createOptimisticAction$1<any>> };
|
|
28
|
+
/**
|
|
29
|
+
* Options for creating a stream DB
|
|
30
|
+
*/
|
|
31
|
+
interface CreateStreamDBOptions<TDef extends StreamStateDefinition = StreamStateDefinition, TActions extends Record<string, ActionDefinition<any>> = Record<string, never>> {
|
|
32
|
+
/** Options for creating a new durable stream. Ignored when `stream` is provided. */
|
|
33
|
+
streamOptions?: DurableStreamOptions;
|
|
34
|
+
/** Pre-existing DurableStream instance to reuse (avoids creating a second connection). */
|
|
35
|
+
stream?: DurableStream;
|
|
36
|
+
/** Live read mode used by the StreamDB consumer. Defaults to true. */
|
|
37
|
+
live?: LiveMode;
|
|
38
|
+
/** The stream state definition */
|
|
39
|
+
state: TDef;
|
|
40
|
+
/** Optional factory function to create actions with db and stream context */
|
|
41
|
+
actions?: ActionFactory<TDef, TActions>;
|
|
42
|
+
/** Called for every ChangeEvent as it flows through the stream consumer. */
|
|
43
|
+
onEvent?: (event: ChangeEvent) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Called once per consumed stream batch before items are dispatched.
|
|
46
|
+
* Useful when external consumers need batch metadata available during
|
|
47
|
+
* downstream collection/effect processing.
|
|
48
|
+
*/
|
|
49
|
+
onBeforeBatch?: (batch: JsonBatch<StateEvent>) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Called once per consumed stream batch after items have been dispatched.
|
|
52
|
+
* Useful for tracking safe offsets for external ack/lease protocols.
|
|
53
|
+
*/
|
|
54
|
+
onBatch?: (batch: JsonBatch<StateEvent>) => void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Extract the value type from a CollectionDefinition
|
|
58
|
+
*/
|
|
59
|
+
type ExtractCollectionType<T extends CollectionDefinition> = T extends CollectionDefinition<infer U> ? U : unknown;
|
|
60
|
+
/**
|
|
61
|
+
* Map collection definitions to TanStack DB Collection types
|
|
62
|
+
*/
|
|
63
|
+
type CollectionMap<TDef extends StreamStateDefinition> = { [K in keyof TDef]: Collection$1<ExtractCollectionType<TDef[K]> & object, string> };
|
|
64
|
+
/**
|
|
65
|
+
* The StreamDB interface - provides typed access to collections
|
|
66
|
+
*/
|
|
67
|
+
type StreamDB<TDef extends StreamStateDefinition> = {
|
|
68
|
+
collections: CollectionMap<TDef>;
|
|
69
|
+
} & StreamDBMethods;
|
|
70
|
+
/**
|
|
71
|
+
* StreamDB with actions
|
|
72
|
+
*/
|
|
73
|
+
type StreamDBWithActions<TDef extends StreamStateDefinition, TActions extends Record<string, ActionDefinition<any>>> = StreamDB<TDef> & {
|
|
74
|
+
actions: ActionMap<TActions>;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Utility methods available on StreamDB
|
|
78
|
+
*/
|
|
79
|
+
interface StreamDBUtils {
|
|
80
|
+
/**
|
|
81
|
+
* Wait for a specific transaction ID to be synced through the stream
|
|
82
|
+
* @param txid The transaction ID to wait for (UUID string)
|
|
83
|
+
* @param timeout Optional timeout in milliseconds (defaults to 5000ms)
|
|
84
|
+
* @returns Promise that resolves when the txid is synced
|
|
85
|
+
*/
|
|
86
|
+
awaitTxId: (txid: string, timeout?: number) => Promise<void>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Methods available on a StreamDB instance
|
|
90
|
+
*/
|
|
91
|
+
interface StreamDBMethods {
|
|
92
|
+
/**
|
|
93
|
+
* The underlying DurableStream instance
|
|
94
|
+
*/
|
|
95
|
+
stream: DurableStream;
|
|
96
|
+
/**
|
|
97
|
+
* Current stream offset (tracks the last consumed position).
|
|
98
|
+
*/
|
|
99
|
+
readonly offset: string;
|
|
100
|
+
/**
|
|
101
|
+
* Preload all collections by consuming the stream until up-to-date
|
|
102
|
+
*/
|
|
103
|
+
preload: () => Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* Close the stream connection and cleanup
|
|
106
|
+
*/
|
|
107
|
+
close: () => void;
|
|
108
|
+
/**
|
|
109
|
+
* Utility methods for advanced stream operations
|
|
110
|
+
*/
|
|
111
|
+
utils: StreamDBUtils;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Build a TanStack collection id for a StreamDB collection.
|
|
115
|
+
*
|
|
116
|
+
* Collection ids must be unique per source stream, not just per schema key,
|
|
117
|
+
* otherwise joining the same collection name from two different streams can
|
|
118
|
+
* collapse to one logical source inside TanStack DB.
|
|
119
|
+
*/
|
|
120
|
+
declare function getStreamDBCollectionId(streamUrl: string, collectionName: string): string;
|
|
121
|
+
/**
|
|
122
|
+
* Create a stream-backed database with TanStack DB collections
|
|
123
|
+
*
|
|
124
|
+
* This function is synchronous - it creates the stream handle and collections
|
|
125
|
+
* but does not start the stream connection. Call `db.preload()` to connect
|
|
126
|
+
* and sync initial data.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* const stateSchema = createStateSchema({
|
|
131
|
+
* users: { schema: userSchema, type: "user", primaryKey: "id" },
|
|
132
|
+
* messages: { schema: messageSchema, type: "message", primaryKey: "id" },
|
|
133
|
+
* })
|
|
134
|
+
*
|
|
135
|
+
* // Create a stream DB (synchronous - stream is created lazily on preload)
|
|
136
|
+
* const db = createStreamDB({
|
|
137
|
+
* streamOptions: {
|
|
138
|
+
* url: "https://api.example.com/streams/my-stream",
|
|
139
|
+
* contentType: "application/json",
|
|
140
|
+
* },
|
|
141
|
+
* state: stateSchema,
|
|
142
|
+
* })
|
|
143
|
+
*
|
|
144
|
+
* // preload() creates the stream and loads initial data
|
|
145
|
+
* await db.preload()
|
|
146
|
+
* const user = await db.collections.users.get("123")
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
declare function createStreamDB<TDef extends StreamStateDefinition, TActions extends Record<string, ActionDefinition<any>> = Record<string, never>>(options: CreateStreamDBOptions<TDef, TActions>): TActions extends Record<string, never> ? StreamDB<TDef> : StreamDBWithActions<TDef, TActions>;
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
export { ActionDefinition, ActionFactory, ActionMap, ChangeEvent, ChangeHeaders, Collection, CollectionDefinition, CollectionEventHelpers, CollectionWithHelpers, ControlEvent, CreateStreamDBOptions, MaterializedState, Operation, Row, StateEvent, StateSchema, StreamDB, StreamDBMethods, StreamDBUtils, StreamDBWithActions, StreamStateDefinition, SyncConfig, Value, and, avg, coalesce, concat, count, createCollection, createLiveQueryCollection, createOptimisticAction, createStateSchema, createStreamDB, createTransaction, deepEquals, eq, getStreamDBCollectionId, gt, gte, ilike, inArray, isChangeEvent, isControlEvent, isNull, isUndefined, like, localOnlyCollectionOptions, lt, lte, max, min, not, or, queryOnce, sum, toArray };
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { MaterializedState, createStateSchema, isChangeEvent, isControlEvent } from "./src-VTyL9Eij.js";
|
|
2
|
+
import { and, avg, coalesce, concat, count, createCollection, createCollection as createCollection$1, createLiveQueryCollection, createOptimisticAction, createOptimisticAction as createOptimisticAction$1, createTransaction, deepEquals, deepEquals as deepEquals$1, eq, gt, gte, ilike, inArray, isNull, isUndefined, like, localOnlyCollectionOptions, lt, lte, max, min, not, or, queryOnce, sum, toArray } from "@tanstack/db";
|
|
3
|
+
import { DurableStream } from "@durable-streams/client";
|
|
4
|
+
|
|
5
|
+
//#region src/stream-db.ts
|
|
6
|
+
/**
|
|
7
|
+
* Build a TanStack collection id for a StreamDB collection.
|
|
8
|
+
*
|
|
9
|
+
* Collection ids must be unique per source stream, not just per schema key,
|
|
10
|
+
* otherwise joining the same collection name from two different streams can
|
|
11
|
+
* collapse to one logical source inside TanStack DB.
|
|
12
|
+
*/
|
|
13
|
+
function getStreamDBCollectionId(streamUrl, collectionName) {
|
|
14
|
+
return `stream-db:${streamUrl}:${collectionName}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Internal event dispatcher that routes stream events to collection handlers
|
|
18
|
+
*/
|
|
19
|
+
var EventDispatcher = class {
|
|
20
|
+
/** Map from event type to collection handler */
|
|
21
|
+
handlers = new Map();
|
|
22
|
+
/** Handlers that have pending writes (need commit) */
|
|
23
|
+
pendingHandlers = new Set();
|
|
24
|
+
/** Whether we've received the initial up-to-date signal */
|
|
25
|
+
isUpToDate = false;
|
|
26
|
+
/** Resolvers and rejecters for preload promises */
|
|
27
|
+
preloadResolvers = [];
|
|
28
|
+
preloadRejecters = [];
|
|
29
|
+
/** Set of all txids that have been seen and committed */
|
|
30
|
+
seenTxids = new Set();
|
|
31
|
+
/** Txids collected during current batch (before commit) */
|
|
32
|
+
pendingTxids = new Set();
|
|
33
|
+
/** Resolvers waiting for specific txids */
|
|
34
|
+
txidResolvers = new Map();
|
|
35
|
+
/** Track existing keys per collection for upsert logic */
|
|
36
|
+
existingKeys = new Map();
|
|
37
|
+
/** Global sequence counter for insertion ordering */
|
|
38
|
+
seq = 0;
|
|
39
|
+
comparableRow(row) {
|
|
40
|
+
const clone = { ...row };
|
|
41
|
+
delete clone._seq;
|
|
42
|
+
return clone;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Register a handler for a specific event type
|
|
46
|
+
*/
|
|
47
|
+
registerHandler(eventType, handler) {
|
|
48
|
+
this.handlers.set(eventType, handler);
|
|
49
|
+
if (!this.existingKeys.has(eventType)) this.existingKeys.set(eventType, new Set());
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Dispatch a change event to the appropriate collection.
|
|
53
|
+
* Writes are buffered until commit() is called via markUpToDate().
|
|
54
|
+
*/
|
|
55
|
+
dispatchChange(event, cursor) {
|
|
56
|
+
if (!isChangeEvent(event)) return;
|
|
57
|
+
const eventCursor = event.headers.offset ?? cursor;
|
|
58
|
+
if (event.headers.txid && typeof event.headers.txid === `string`) this.pendingTxids.add(event.headers.txid);
|
|
59
|
+
const handler = this.handlers.get(event.type);
|
|
60
|
+
if (!handler) return;
|
|
61
|
+
let operation = event.headers.operation;
|
|
62
|
+
if (operation !== `delete`) {
|
|
63
|
+
if (typeof event.value !== `object` || event.value === null) throw new Error(`StreamDB collections require object values; got ${typeof event.value} for type=${event.type}, key=${event.key}`);
|
|
64
|
+
}
|
|
65
|
+
const originalValue = event.value ?? {};
|
|
66
|
+
const value = { ...originalValue };
|
|
67
|
+
value[handler.primaryKey] = event.key;
|
|
68
|
+
value._seq = this.seq++;
|
|
69
|
+
if (!this.pendingHandlers.has(handler)) {
|
|
70
|
+
handler.begin();
|
|
71
|
+
this.pendingHandlers.add(handler);
|
|
72
|
+
}
|
|
73
|
+
if (operation === `upsert`) {
|
|
74
|
+
const keys$1 = this.existingKeys.get(event.type);
|
|
75
|
+
const existing = keys$1?.has(event.key);
|
|
76
|
+
operation = existing ? `update` : `insert`;
|
|
77
|
+
}
|
|
78
|
+
const keys = this.existingKeys.get(event.type);
|
|
79
|
+
if (operation === `insert` && keys?.has(event.key)) operation = `update`;
|
|
80
|
+
else if (operation === `insert` && typeof event.key === `string`) {
|
|
81
|
+
const existingValue = handler.read(event.key);
|
|
82
|
+
if (existingValue && deepEquals$1(this.comparableRow(existingValue), this.comparableRow(value))) operation = `update`;
|
|
83
|
+
}
|
|
84
|
+
if (operation === `insert` || operation === `update`) keys?.add(event.key);
|
|
85
|
+
else keys?.delete(event.key);
|
|
86
|
+
try {
|
|
87
|
+
handler.write(value, operation, eventCursor);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error(`[StreamDB] Error in handler.write():`, error);
|
|
90
|
+
console.error(`[StreamDB] Event that caused error:`, {
|
|
91
|
+
type: event.type,
|
|
92
|
+
key: event.key,
|
|
93
|
+
operation
|
|
94
|
+
});
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Handle control events from the stream JSON items
|
|
100
|
+
*/
|
|
101
|
+
dispatchControl(event) {
|
|
102
|
+
if (!isControlEvent(event)) return;
|
|
103
|
+
switch (event.headers.control) {
|
|
104
|
+
case `reset`:
|
|
105
|
+
for (const handler of this.handlers.values()) handler.truncate();
|
|
106
|
+
for (const keys of this.existingKeys.values()) keys.clear();
|
|
107
|
+
this.pendingHandlers.clear();
|
|
108
|
+
this.isUpToDate = false;
|
|
109
|
+
break;
|
|
110
|
+
case `snapshot-start`:
|
|
111
|
+
case `snapshot-end`: break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Commit all pending writes and handle up-to-date signal
|
|
116
|
+
*/
|
|
117
|
+
markUpToDate() {
|
|
118
|
+
for (const handler of this.pendingHandlers) try {
|
|
119
|
+
handler.commit();
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(`[StreamDB] Error in handler.commit():`, error);
|
|
122
|
+
if (error instanceof Error && error.message.includes(`already exists in the collection`) && error.message.includes(`live-query`)) {
|
|
123
|
+
console.warn(`[StreamDB] Known TanStack DB groupBy bug detected - continuing despite error`);
|
|
124
|
+
console.warn(`[StreamDB] Queries with groupBy may show stale data until fixed`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
this.pendingHandlers.clear();
|
|
130
|
+
for (const txid of this.pendingTxids) {
|
|
131
|
+
this.seenTxids.add(txid);
|
|
132
|
+
const resolvers = this.txidResolvers.get(txid);
|
|
133
|
+
if (resolvers) {
|
|
134
|
+
for (const { resolve, timeoutId } of resolvers) {
|
|
135
|
+
clearTimeout(timeoutId);
|
|
136
|
+
resolve();
|
|
137
|
+
}
|
|
138
|
+
this.txidResolvers.delete(txid);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
this.pendingTxids.clear();
|
|
142
|
+
if (!this.isUpToDate) {
|
|
143
|
+
this.isUpToDate = true;
|
|
144
|
+
for (const handler of this.handlers.values()) handler.markReady();
|
|
145
|
+
for (const resolve of this.preloadResolvers) resolve();
|
|
146
|
+
this.preloadResolvers = [];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Wait for the stream to reach up-to-date state
|
|
151
|
+
*/
|
|
152
|
+
waitForUpToDate() {
|
|
153
|
+
if (this.isUpToDate) return Promise.resolve();
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
this.preloadResolvers.push(resolve);
|
|
156
|
+
this.preloadRejecters.push(reject);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Reject all waiting preload promises with an error
|
|
161
|
+
*/
|
|
162
|
+
rejectAll(error) {
|
|
163
|
+
for (const reject of this.preloadRejecters) reject(error);
|
|
164
|
+
this.preloadResolvers = [];
|
|
165
|
+
this.preloadRejecters = [];
|
|
166
|
+
for (const resolvers of this.txidResolvers.values()) for (const { reject, timeoutId } of resolvers) {
|
|
167
|
+
clearTimeout(timeoutId);
|
|
168
|
+
reject(error);
|
|
169
|
+
}
|
|
170
|
+
this.txidResolvers.clear();
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Check if we've received up-to-date
|
|
174
|
+
*/
|
|
175
|
+
get ready() {
|
|
176
|
+
return this.isUpToDate;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Wait for a specific txid to be seen in the stream
|
|
180
|
+
*/
|
|
181
|
+
awaitTxId(txid, timeout = 5e3) {
|
|
182
|
+
if (this.seenTxids.has(txid)) return Promise.resolve();
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
const timeoutId = setTimeout(() => {
|
|
185
|
+
const resolvers = this.txidResolvers.get(txid);
|
|
186
|
+
if (resolvers) {
|
|
187
|
+
const index = resolvers.findIndex((r) => r.timeoutId === timeoutId);
|
|
188
|
+
if (index !== -1) resolvers.splice(index, 1);
|
|
189
|
+
if (resolvers.length === 0) this.txidResolvers.delete(txid);
|
|
190
|
+
}
|
|
191
|
+
reject(new Error(`Timeout waiting for txid: ${txid}`));
|
|
192
|
+
}, timeout);
|
|
193
|
+
if (!this.txidResolvers.has(txid)) this.txidResolvers.set(txid, []);
|
|
194
|
+
this.txidResolvers.get(txid).push({
|
|
195
|
+
resolve,
|
|
196
|
+
reject,
|
|
197
|
+
timeoutId
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Create a sync config for a stream-backed collection
|
|
204
|
+
*/
|
|
205
|
+
function createStreamSyncConfig(eventType, dispatcher, primaryKey, read) {
|
|
206
|
+
return { sync: ({ begin, write, commit, markReady, truncate }) => {
|
|
207
|
+
dispatcher.registerHandler(eventType, {
|
|
208
|
+
begin,
|
|
209
|
+
write: (value, type, _cursor) => {
|
|
210
|
+
write({
|
|
211
|
+
value,
|
|
212
|
+
type
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
read: (key) => read(key),
|
|
216
|
+
commit,
|
|
217
|
+
markReady,
|
|
218
|
+
truncate,
|
|
219
|
+
primaryKey
|
|
220
|
+
});
|
|
221
|
+
if (dispatcher.ready) markReady();
|
|
222
|
+
return () => {};
|
|
223
|
+
} };
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Create a stream-backed database with TanStack DB collections
|
|
227
|
+
*
|
|
228
|
+
* This function is synchronous - it creates the stream handle and collections
|
|
229
|
+
* but does not start the stream connection. Call `db.preload()` to connect
|
|
230
|
+
* and sync initial data.
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```typescript
|
|
234
|
+
* const stateSchema = createStateSchema({
|
|
235
|
+
* users: { schema: userSchema, type: "user", primaryKey: "id" },
|
|
236
|
+
* messages: { schema: messageSchema, type: "message", primaryKey: "id" },
|
|
237
|
+
* })
|
|
238
|
+
*
|
|
239
|
+
* // Create a stream DB (synchronous - stream is created lazily on preload)
|
|
240
|
+
* const db = createStreamDB({
|
|
241
|
+
* streamOptions: {
|
|
242
|
+
* url: "https://api.example.com/streams/my-stream",
|
|
243
|
+
* contentType: "application/json",
|
|
244
|
+
* },
|
|
245
|
+
* state: stateSchema,
|
|
246
|
+
* })
|
|
247
|
+
*
|
|
248
|
+
* // preload() creates the stream and loads initial data
|
|
249
|
+
* await db.preload()
|
|
250
|
+
* const user = await db.collections.users.get("123")
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
function createStreamDB(options) {
|
|
254
|
+
const { streamOptions, state, actions: actionsFactory, live = true, onEvent, onBeforeBatch, onBatch } = options;
|
|
255
|
+
const stream = options.stream ?? (() => {
|
|
256
|
+
if (!streamOptions) throw new Error(`createStreamDB requires stream or streamOptions`);
|
|
257
|
+
return new DurableStream(streamOptions);
|
|
258
|
+
})();
|
|
259
|
+
const dispatcher = new EventDispatcher();
|
|
260
|
+
const streamIdentity = stream.url;
|
|
261
|
+
const collectionInstances = {};
|
|
262
|
+
for (const [name, definition] of Object.entries(state)) {
|
|
263
|
+
let collection = createCollection$1({
|
|
264
|
+
id: getStreamDBCollectionId(streamIdentity, name),
|
|
265
|
+
schema: definition.schema,
|
|
266
|
+
getKey: (item) => String(item[definition.primaryKey]),
|
|
267
|
+
sync: createStreamSyncConfig(definition.type, dispatcher, definition.primaryKey, (key) => collection.get(key)),
|
|
268
|
+
startSync: true,
|
|
269
|
+
gcTime: 0
|
|
270
|
+
});
|
|
271
|
+
collectionInstances[name] = collection;
|
|
272
|
+
}
|
|
273
|
+
let streamResponse = null;
|
|
274
|
+
const abortController = new AbortController();
|
|
275
|
+
let consumerStarted = false;
|
|
276
|
+
let lastConsumedOffset = `-1`;
|
|
277
|
+
const isAbortLikeError = (err) => {
|
|
278
|
+
if (abortController.signal.aborted) return true;
|
|
279
|
+
if (!(err instanceof Error)) return false;
|
|
280
|
+
return err.name === `AbortError` || err.name === `FetchBackoffAbortError` || err.message === `Stream request was aborted`;
|
|
281
|
+
};
|
|
282
|
+
/**
|
|
283
|
+
* Start the stream consumer (called lazily on first preload)
|
|
284
|
+
*/
|
|
285
|
+
const startConsumer = async () => {
|
|
286
|
+
if (consumerStarted) return;
|
|
287
|
+
consumerStarted = true;
|
|
288
|
+
streamResponse = await stream.stream({
|
|
289
|
+
live,
|
|
290
|
+
json: true,
|
|
291
|
+
signal: abortController.signal
|
|
292
|
+
});
|
|
293
|
+
streamResponse.closed.catch((err) => {
|
|
294
|
+
if (isAbortLikeError(err)) return void 0;
|
|
295
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
296
|
+
console.error(`[StreamDB] Stream consumer closed unexpectedly:`, error);
|
|
297
|
+
dispatcher.rejectAll(error);
|
|
298
|
+
return void 0;
|
|
299
|
+
});
|
|
300
|
+
lastConsumedOffset = streamResponse.offset;
|
|
301
|
+
streamResponse.subscribeJson((batch) => {
|
|
302
|
+
try {
|
|
303
|
+
lastConsumedOffset = batch.offset;
|
|
304
|
+
onBeforeBatch?.(batch);
|
|
305
|
+
for (const event of batch.items) if (isChangeEvent(event)) {
|
|
306
|
+
dispatcher.dispatchChange(event, batch.offset);
|
|
307
|
+
onEvent?.(event);
|
|
308
|
+
} else if (isControlEvent(event)) dispatcher.dispatchControl(event);
|
|
309
|
+
onBatch?.(batch);
|
|
310
|
+
if (batch.upToDate || dispatcher.ready) dispatcher.markUpToDate();
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error(`[StreamDB] Error processing batch:`, error);
|
|
313
|
+
dispatcher.rejectAll(error);
|
|
314
|
+
abortController.abort();
|
|
315
|
+
}
|
|
316
|
+
return Promise.resolve();
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
const dbMethods = {
|
|
320
|
+
stream,
|
|
321
|
+
get offset() {
|
|
322
|
+
return lastConsumedOffset;
|
|
323
|
+
},
|
|
324
|
+
preload: async () => {
|
|
325
|
+
await startConsumer();
|
|
326
|
+
await dispatcher.waitForUpToDate();
|
|
327
|
+
},
|
|
328
|
+
close: () => {
|
|
329
|
+
dispatcher.rejectAll(new Error(`StreamDB closed`));
|
|
330
|
+
abortController.abort();
|
|
331
|
+
},
|
|
332
|
+
utils: { awaitTxId: (txid, timeout) => dispatcher.awaitTxId(txid, timeout) }
|
|
333
|
+
};
|
|
334
|
+
const db = Object.create(null);
|
|
335
|
+
Object.defineProperty(db, `collections`, {
|
|
336
|
+
value: collectionInstances,
|
|
337
|
+
enumerable: true,
|
|
338
|
+
configurable: false,
|
|
339
|
+
writable: false
|
|
340
|
+
});
|
|
341
|
+
Object.defineProperties(db, Object.getOwnPropertyDescriptors(dbMethods));
|
|
342
|
+
if (actionsFactory) {
|
|
343
|
+
const actionDefs = actionsFactory({
|
|
344
|
+
db,
|
|
345
|
+
stream
|
|
346
|
+
});
|
|
347
|
+
const wrappedActions = {};
|
|
348
|
+
for (const [name, def] of Object.entries(actionDefs)) wrappedActions[name] = createOptimisticAction$1({
|
|
349
|
+
onMutate: def.onMutate,
|
|
350
|
+
mutationFn: def.mutationFn
|
|
351
|
+
});
|
|
352
|
+
Object.defineProperty(db, `actions`, {
|
|
353
|
+
value: wrappedActions,
|
|
354
|
+
enumerable: true,
|
|
355
|
+
configurable: false,
|
|
356
|
+
writable: false
|
|
357
|
+
});
|
|
358
|
+
return db;
|
|
359
|
+
}
|
|
360
|
+
return db;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
//#endregion
|
|
364
|
+
export { MaterializedState, and, avg, coalesce, concat, count, createCollection, createLiveQueryCollection, createOptimisticAction, createStateSchema, createStreamDB, createTransaction, deepEquals, eq, getStreamDBCollectionId, gt, gte, ilike, inArray, isChangeEvent, isControlEvent, isNull, isUndefined, like, localOnlyCollectionOptions, lt, lte, max, min, not, or, queryOnce, sum, toArray };
|