@tanstack/db 0.0.11 → 0.0.12
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/dist/cjs/collection.cjs +9 -49
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +29 -30
- package/dist/cjs/index.cjs +0 -1
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/query/compiled-query.cjs +17 -5
- package/dist/cjs/query/compiled-query.cjs.map +1 -1
- package/dist/cjs/types.d.cts +39 -10
- package/dist/esm/collection.d.ts +29 -30
- package/dist/esm/collection.js +10 -50
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/index.js +1 -2
- package/dist/esm/query/compiled-query.js +17 -5
- package/dist/esm/query/compiled-query.js.map +1 -1
- package/dist/esm/types.d.ts +39 -10
- package/package.json +1 -1
- package/src/collection.ts +66 -121
- package/src/query/compiled-query.ts +43 -7
- package/src/types.ts +77 -8
|
@@ -18,7 +18,12 @@ class CompiledQuery {
|
|
|
18
18
|
const inputs = Object.fromEntries(
|
|
19
19
|
Object.entries(collections).map(([key]) => [key, graph.newInput()])
|
|
20
20
|
);
|
|
21
|
-
const sync = ({
|
|
21
|
+
const sync = ({
|
|
22
|
+
begin,
|
|
23
|
+
write,
|
|
24
|
+
commit,
|
|
25
|
+
collection
|
|
26
|
+
}) => {
|
|
22
27
|
compileQueryPipeline(
|
|
23
28
|
query,
|
|
24
29
|
inputs
|
|
@@ -42,12 +47,17 @@ class CompiledQuery {
|
|
|
42
47
|
}, /* @__PURE__ */ new Map()).forEach((changes, rawKey) => {
|
|
43
48
|
const { deletes, inserts, value } = changes;
|
|
44
49
|
const valueWithKey = { ...value, _key: rawKey };
|
|
45
|
-
if (inserts &&
|
|
50
|
+
if (inserts && deletes === 0) {
|
|
46
51
|
write({
|
|
47
52
|
value: valueWithKey,
|
|
48
53
|
type: `insert`
|
|
49
54
|
});
|
|
50
|
-
} else if (
|
|
55
|
+
} else if (
|
|
56
|
+
// Insert & update(s) (updates are a delete & insert)
|
|
57
|
+
inserts > deletes || // Just update(s) but the item is already in the collection (so
|
|
58
|
+
// was inserted previously).
|
|
59
|
+
inserts === deletes && collection.has(valueWithKey._key)
|
|
60
|
+
) {
|
|
51
61
|
write({
|
|
52
62
|
value: valueWithKey,
|
|
53
63
|
type: `update`
|
|
@@ -57,6 +67,10 @@ class CompiledQuery {
|
|
|
57
67
|
value: valueWithKey,
|
|
58
68
|
type: `delete`
|
|
59
69
|
});
|
|
70
|
+
} else {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`This should never happen ${JSON.stringify(changes)}`
|
|
73
|
+
);
|
|
60
74
|
}
|
|
61
75
|
});
|
|
62
76
|
commit();
|
|
@@ -67,8 +81,6 @@ class CompiledQuery {
|
|
|
67
81
|
this.graph = graph;
|
|
68
82
|
this.inputs = inputs;
|
|
69
83
|
this.resultCollection = createCollection({
|
|
70
|
-
id: crypto.randomUUID(),
|
|
71
|
-
// TODO: remove when we don't require any more
|
|
72
84
|
getKey: (val) => {
|
|
73
85
|
return val._key;
|
|
74
86
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compiled-query.js","sources":["../../../src/query/compiled-query.ts"],"sourcesContent":["import { D2, MultiSet, output } from \"@electric-sql/d2mini\"\nimport { createCollection } from \"../collection.js\"\nimport { compileQueryPipeline } from \"./pipeline-compiler.js\"\nimport type { Collection } from \"../collection.js\"\nimport type { ChangeMessage, SyncConfig } from \"../types.js\"\nimport type {\n IStreamBuilder,\n MultiSetArray,\n RootStreamBuilder,\n} from \"@electric-sql/d2mini\"\nimport type { QueryBuilder, ResultsFromContext } from \"./query-builder.js\"\nimport type { Context, Schema } from \"./types.js\"\n\nexport function compileQuery<TContext extends Context<Schema>>(\n queryBuilder: QueryBuilder<TContext>\n) {\n return new CompiledQuery<\n ResultsFromContext<TContext> & { _key?: string | number }\n >(queryBuilder)\n}\n\nexport class CompiledQuery<TResults extends object = Record<string, unknown>> {\n private graph: D2\n private inputs: Record<string, RootStreamBuilder<any>>\n private inputCollections: Record<string, Collection<any>>\n private resultCollection: Collection<TResults>\n public state: `compiled` | `running` | `stopped` = `compiled`\n private unsubscribeCallbacks: Array<() => void> = []\n\n constructor(queryBuilder: QueryBuilder<Context<Schema>>) {\n const query = queryBuilder._query\n const collections = query.collections\n\n if (!collections) {\n throw new Error(`No collections provided`)\n }\n\n this.inputCollections = collections\n\n const graph = new D2()\n const inputs = Object.fromEntries(\n Object.entries(collections).map(([key]) => [key, graph.newInput<any>()])\n )\n\n const sync: SyncConfig<TResults>[`sync`] = ({
|
|
1
|
+
{"version":3,"file":"compiled-query.js","sources":["../../../src/query/compiled-query.ts"],"sourcesContent":["import { D2, MultiSet, output } from \"@electric-sql/d2mini\"\nimport { createCollection } from \"../collection.js\"\nimport { compileQueryPipeline } from \"./pipeline-compiler.js\"\nimport type { Collection } from \"../collection.js\"\nimport type { ChangeMessage, ResolveType, SyncConfig } from \"../types.js\"\nimport type {\n IStreamBuilder,\n MultiSetArray,\n RootStreamBuilder,\n} from \"@electric-sql/d2mini\"\nimport type { QueryBuilder, ResultsFromContext } from \"./query-builder.js\"\nimport type { Context, Schema } from \"./types.js\"\n\nexport function compileQuery<TContext extends Context<Schema>>(\n queryBuilder: QueryBuilder<TContext>\n) {\n return new CompiledQuery<\n ResultsFromContext<TContext> & { _key?: string | number }\n >(queryBuilder)\n}\n\nexport class CompiledQuery<TResults extends object = Record<string, unknown>> {\n private graph: D2\n private inputs: Record<string, RootStreamBuilder<any>>\n private inputCollections: Record<string, Collection<any>>\n private resultCollection: Collection<TResults>\n public state: `compiled` | `running` | `stopped` = `compiled`\n private unsubscribeCallbacks: Array<() => void> = []\n\n constructor(queryBuilder: QueryBuilder<Context<Schema>>) {\n const query = queryBuilder._query\n const collections = query.collections\n\n if (!collections) {\n throw new Error(`No collections provided`)\n }\n\n this.inputCollections = collections\n\n const graph = new D2()\n const inputs = Object.fromEntries(\n Object.entries(collections).map(([key]) => [key, graph.newInput<any>()])\n )\n\n // Use TResults directly to ensure type compatibility\n const sync: SyncConfig<TResults>[`sync`] = ({\n begin,\n write,\n commit,\n collection,\n }) => {\n compileQueryPipeline<IStreamBuilder<[unknown, TResults]>>(\n query,\n inputs\n ).pipe(\n output((data) => {\n begin()\n data\n .getInner()\n .reduce((acc, [[key, value], multiplicity]) => {\n const changes = acc.get(key) || {\n deletes: 0,\n inserts: 0,\n value,\n }\n if (multiplicity < 0) {\n changes.deletes += Math.abs(multiplicity)\n } else if (multiplicity > 0) {\n changes.inserts += multiplicity\n changes.value = value\n }\n acc.set(key, changes)\n return acc\n }, new Map<unknown, { deletes: number; inserts: number; value: TResults }>())\n .forEach((changes, rawKey) => {\n const { deletes, inserts, value } = changes\n const valueWithKey = { ...value, _key: rawKey }\n\n // Simple singular insert.\n if (inserts && deletes === 0) {\n write({\n value: valueWithKey,\n type: `insert`,\n })\n } else if (\n // Insert & update(s) (updates are a delete & insert)\n inserts > deletes ||\n // Just update(s) but the item is already in the collection (so\n // was inserted previously).\n (inserts === deletes &&\n collection.has(valueWithKey._key as string | number))\n ) {\n write({\n value: valueWithKey,\n type: `update`,\n })\n // Only delete is left as an option\n } else if (deletes > 0) {\n write({\n value: valueWithKey,\n type: `delete`,\n })\n } else {\n throw new Error(\n `This should never happen ${JSON.stringify(changes)}`\n )\n }\n })\n commit()\n })\n )\n graph.finalize()\n }\n\n this.graph = graph\n this.inputs = inputs\n this.resultCollection = createCollection<TResults>({\n getKey: (val: unknown) => {\n return (val as any)._key\n },\n sync: {\n sync: sync as unknown as (params: {\n collection: Collection<\n ResolveType<TResults, never, Record<string, unknown>>,\n string | number,\n {}\n >\n begin: () => void\n write: (\n message: Omit<\n ChangeMessage<\n ResolveType<TResults, never, Record<string, unknown>>,\n string | number\n >,\n `key`\n >\n ) => void\n commit: () => void\n }) => void,\n },\n }) as unknown as Collection<TResults, string | number, {}>\n }\n\n get results() {\n return this.resultCollection\n }\n\n private sendChangesToInput(\n inputKey: string,\n changes: Array<ChangeMessage>,\n getKey: (item: ChangeMessage[`value`]) => any\n ) {\n const input = this.inputs[inputKey]!\n const multiSetArray: MultiSetArray<unknown> = []\n for (const change of changes) {\n const key = getKey(change.value)\n if (change.type === `insert`) {\n multiSetArray.push([[key, change.value], 1])\n } else if (change.type === `update`) {\n multiSetArray.push([[key, change.previousValue], -1])\n multiSetArray.push([[key, change.value], 1])\n } else {\n // change.type === `delete`\n multiSetArray.push([[key, change.value], -1])\n }\n }\n input.sendData(new MultiSet(multiSetArray))\n }\n\n private runGraph() {\n this.graph.run()\n }\n\n start() {\n if (this.state === `running`) {\n throw new Error(`Query is already running`)\n } else if (this.state === `stopped`) {\n throw new Error(`Query is stopped`)\n }\n\n // Send initial state\n Object.entries(this.inputCollections).forEach(([key, collection]) => {\n this.sendChangesToInput(\n key,\n collection.currentStateAsChanges(),\n collection.config.getKey\n )\n })\n this.runGraph()\n\n // Subscribe to changes\n Object.entries(this.inputCollections).forEach(([key, collection]) => {\n const unsubscribe = collection.subscribeChanges((changes) => {\n this.sendChangesToInput(key, changes, collection.config.getKey)\n this.runGraph()\n })\n\n this.unsubscribeCallbacks.push(unsubscribe)\n })\n\n this.state = `running`\n return () => {\n this.stop()\n }\n }\n\n stop() {\n this.unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe())\n this.unsubscribeCallbacks = []\n this.state = `stopped`\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -2,6 +2,23 @@ import { IStreamBuilder } from '@electric-sql/d2mini';
|
|
|
2
2
|
import { Collection } from './collection.js';
|
|
3
3
|
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
4
4
|
import { Transaction } from './transactions.js';
|
|
5
|
+
/**
|
|
6
|
+
* Helper type to extract the output type from a standard schema
|
|
7
|
+
*
|
|
8
|
+
* @internal This is used by the type resolution system
|
|
9
|
+
*/
|
|
10
|
+
export type InferSchemaOutput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<T> extends object ? StandardSchemaV1.InferOutput<T> : Record<string, unknown> : Record<string, unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* Helper type to determine the final type based on priority:
|
|
13
|
+
* 1. Explicit generic TExplicit (if not 'unknown')
|
|
14
|
+
* 2. Schema output type (if schema provided)
|
|
15
|
+
* 3. Fallback type TFallback
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* This type is used internally to resolve the collection item type based on the provided generics and schema.
|
|
19
|
+
* Users should not need to use this type directly, but understanding the priority order helps when defining collections.
|
|
20
|
+
*/
|
|
21
|
+
export type ResolveType<TExplicit, TSchema extends StandardSchemaV1 = never, TFallback extends object = Record<string, unknown>> = unknown extends TExplicit ? [TSchema] extends [never] ? TFallback : InferSchemaOutput<TSchema> : TExplicit extends object ? TExplicit : Record<string, unknown>;
|
|
5
22
|
export type TransactionState = `pending` | `persisting` | `completed` | `failed`;
|
|
6
23
|
/**
|
|
7
24
|
* Represents a utility function that can be attached to a collection
|
|
@@ -15,11 +32,11 @@ export type UtilsRecord = Record<string, Fn>;
|
|
|
15
32
|
* Represents a pending mutation within a transaction
|
|
16
33
|
* Contains information about the original and modified data, as well as metadata
|
|
17
34
|
*/
|
|
18
|
-
export interface PendingMutation<T extends object = Record<string, unknown
|
|
35
|
+
export interface PendingMutation<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType> {
|
|
19
36
|
mutationId: string;
|
|
20
|
-
original:
|
|
37
|
+
original: TOperation extends `insert` ? {} : T;
|
|
21
38
|
modified: T;
|
|
22
|
-
changes: Partial<T>;
|
|
39
|
+
changes: TOperation extends `insert` ? T : TOperation extends `delete` ? T : Partial<T>;
|
|
23
40
|
globalKey: string;
|
|
24
41
|
key: any;
|
|
25
42
|
type: OperationType;
|
|
@@ -44,8 +61,8 @@ export type NonEmptyArray<T> = [T, ...Array<T>];
|
|
|
44
61
|
* Utility type for a Transaction with at least one mutation
|
|
45
62
|
* This is used internally by the Transaction.commit method
|
|
46
63
|
*/
|
|
47
|
-
export type TransactionWithMutations<T extends object = Record<string, unknown
|
|
48
|
-
mutations: NonEmptyArray<PendingMutation<T>>;
|
|
64
|
+
export type TransactionWithMutations<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType> = Transaction<T> & {
|
|
65
|
+
mutations: NonEmptyArray<PendingMutation<T, TOperation>>;
|
|
49
66
|
};
|
|
50
67
|
export interface TransactionConfig<T extends object = Record<string, unknown>> {
|
|
51
68
|
/** Unique identifier for the transaction */
|
|
@@ -106,10 +123,22 @@ export interface OperationConfig {
|
|
|
106
123
|
export interface InsertConfig {
|
|
107
124
|
metadata?: Record<string, unknown>;
|
|
108
125
|
}
|
|
109
|
-
export
|
|
126
|
+
export type UpdateMutationFnParams<T extends object = Record<string, unknown>> = {
|
|
127
|
+
transaction: TransactionWithMutations<T, `update`>;
|
|
128
|
+
};
|
|
129
|
+
export type InsertMutationFnParams<T extends object = Record<string, unknown>> = {
|
|
130
|
+
transaction: TransactionWithMutations<T, `insert`>;
|
|
131
|
+
};
|
|
132
|
+
export type DeleteMutationFnParams<T extends object = Record<string, unknown>> = {
|
|
133
|
+
transaction: TransactionWithMutations<T, `delete`>;
|
|
134
|
+
};
|
|
135
|
+
export type InsertMutationFn<T extends object = Record<string, unknown>> = (params: InsertMutationFnParams<T>) => Promise<any>;
|
|
136
|
+
export type UpdateMutationFn<T extends object = Record<string, unknown>> = (params: UpdateMutationFnParams<T>) => Promise<any>;
|
|
137
|
+
export type DeleteMutationFn<T extends object = Record<string, unknown>> = (params: DeleteMutationFnParams<T>) => Promise<any>;
|
|
138
|
+
export interface CollectionConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = StandardSchemaV1> {
|
|
110
139
|
id?: string;
|
|
111
140
|
sync: SyncConfig<T, TKey>;
|
|
112
|
-
schema?:
|
|
141
|
+
schema?: TSchema;
|
|
113
142
|
/**
|
|
114
143
|
* Function to extract the ID from an object
|
|
115
144
|
* This is required for update/delete operations which now only accept IDs
|
|
@@ -125,19 +154,19 @@ export interface CollectionConfig<T extends object = Record<string, unknown>, TK
|
|
|
125
154
|
* @param params Object containing transaction and mutation information
|
|
126
155
|
* @returns Promise resolving to any value
|
|
127
156
|
*/
|
|
128
|
-
onInsert?:
|
|
157
|
+
onInsert?: InsertMutationFn<T>;
|
|
129
158
|
/**
|
|
130
159
|
* Optional asynchronous handler function called before an update operation
|
|
131
160
|
* @param params Object containing transaction and mutation information
|
|
132
161
|
* @returns Promise resolving to any value
|
|
133
162
|
*/
|
|
134
|
-
onUpdate?:
|
|
163
|
+
onUpdate?: UpdateMutationFn<T>;
|
|
135
164
|
/**
|
|
136
165
|
* Optional asynchronous handler function called before a delete operation
|
|
137
166
|
* @param params Object containing transaction and mutation information
|
|
138
167
|
* @returns Promise resolving to any value
|
|
139
168
|
*/
|
|
140
|
-
onDelete?:
|
|
169
|
+
onDelete?: DeleteMutationFn<T>;
|
|
141
170
|
}
|
|
142
171
|
export type ChangesPayload<T extends object = Record<string, unknown>> = Array<ChangeMessage<T>>;
|
|
143
172
|
/**
|
package/package.json
CHANGED
package/src/collection.ts
CHANGED
|
@@ -11,23 +11,16 @@ import type {
|
|
|
11
11
|
OperationConfig,
|
|
12
12
|
OptimisticChangeMessage,
|
|
13
13
|
PendingMutation,
|
|
14
|
+
ResolveType,
|
|
14
15
|
StandardSchema,
|
|
15
16
|
Transaction as TransactionType,
|
|
16
17
|
UtilsRecord,
|
|
17
18
|
} from "./types"
|
|
19
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec"
|
|
18
20
|
|
|
19
21
|
// Store collections in memory
|
|
20
22
|
export const collectionsStore = new Map<string, CollectionImpl<any, any>>()
|
|
21
23
|
|
|
22
|
-
// Map to track loading collections
|
|
23
|
-
const loadingCollectionResolvers = new Map<
|
|
24
|
-
string,
|
|
25
|
-
{
|
|
26
|
-
promise: Promise<CollectionImpl<any, any>>
|
|
27
|
-
resolve: (value: CollectionImpl<any, any>) => void
|
|
28
|
-
}
|
|
29
|
-
>()
|
|
30
|
-
|
|
31
24
|
interface PendingSyncedTransaction<T extends object = Record<string, unknown>> {
|
|
32
25
|
committed: boolean
|
|
33
26
|
operations: Array<OptimisticChangeMessage<T>>
|
|
@@ -36,6 +29,7 @@ interface PendingSyncedTransaction<T extends object = Record<string, unknown>> {
|
|
|
36
29
|
/**
|
|
37
30
|
* Enhanced Collection interface that includes both data type T and utilities TUtils
|
|
38
31
|
* @template T - The type of items in the collection
|
|
32
|
+
* @template TKey - The type of the key for the collection
|
|
39
33
|
* @template TUtils - The utilities record type
|
|
40
34
|
*/
|
|
41
35
|
export interface Collection<
|
|
@@ -49,20 +43,53 @@ export interface Collection<
|
|
|
49
43
|
/**
|
|
50
44
|
* Creates a new Collection instance with the given configuration
|
|
51
45
|
*
|
|
52
|
-
* @template
|
|
46
|
+
* @template TExplicit - The explicit type of items in the collection (highest priority)
|
|
53
47
|
* @template TKey - The type of the key for the collection
|
|
54
48
|
* @template TUtils - The utilities record type
|
|
49
|
+
* @template TSchema - The schema type for validation and type inference (second priority)
|
|
50
|
+
* @template TFallback - The fallback type if no explicit or schema type is provided
|
|
55
51
|
* @param options - Collection options with optional utilities
|
|
56
52
|
* @returns A new Collection with utilities exposed both at top level and under .utils
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* // Using explicit type
|
|
56
|
+
* const todos = createCollection<Todo>({
|
|
57
|
+
* getKey: (todo) => todo.id,
|
|
58
|
+
* sync: { sync: () => {} }
|
|
59
|
+
* })
|
|
60
|
+
*
|
|
61
|
+
* // Using schema for type inference (preferred as it also gives you client side validation)
|
|
62
|
+
* const todoSchema = z.object({
|
|
63
|
+
* id: z.string(),
|
|
64
|
+
* title: z.string(),
|
|
65
|
+
* completed: z.boolean()
|
|
66
|
+
* })
|
|
67
|
+
*
|
|
68
|
+
* const todos = createCollection({
|
|
69
|
+
* schema: todoSchema,
|
|
70
|
+
* getKey: (todo) => todo.id,
|
|
71
|
+
* sync: { sync: () => {} }
|
|
72
|
+
* })
|
|
73
|
+
*
|
|
74
|
+
* // Note: You must provide either an explicit type or a schema, but not both
|
|
57
75
|
*/
|
|
58
76
|
export function createCollection<
|
|
59
|
-
|
|
77
|
+
TExplicit = unknown,
|
|
60
78
|
TKey extends string | number = string | number,
|
|
61
79
|
TUtils extends UtilsRecord = {},
|
|
80
|
+
TSchema extends StandardSchemaV1 = StandardSchemaV1,
|
|
81
|
+
TFallback extends object = Record<string, unknown>,
|
|
62
82
|
>(
|
|
63
|
-
options: CollectionConfig<
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
options: CollectionConfig<
|
|
84
|
+
ResolveType<TExplicit, TSchema, TFallback>,
|
|
85
|
+
TKey,
|
|
86
|
+
TSchema
|
|
87
|
+
> & { utils?: TUtils }
|
|
88
|
+
): Collection<ResolveType<TExplicit, TSchema, TFallback>, TKey, TUtils> {
|
|
89
|
+
const collection = new CollectionImpl<
|
|
90
|
+
ResolveType<TExplicit, TSchema, TFallback>,
|
|
91
|
+
TKey
|
|
92
|
+
>(options)
|
|
66
93
|
|
|
67
94
|
// Copy utils to both top level and .utils namespace
|
|
68
95
|
if (options.utils) {
|
|
@@ -71,98 +98,11 @@ export function createCollection<
|
|
|
71
98
|
collection.utils = {} as TUtils
|
|
72
99
|
}
|
|
73
100
|
|
|
74
|
-
return collection as Collection<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
* Returns a promise that resolves once the sync tool has done its first commit (initial sync is finished)
|
|
80
|
-
* If the collection has already loaded, it resolves immediately
|
|
81
|
-
*
|
|
82
|
-
* This function is useful in route loaders or similar pre-rendering scenarios where you want
|
|
83
|
-
* to ensure data is available before a route transition completes. It uses the same shared collection
|
|
84
|
-
* instance that will be used by useCollection, ensuring data consistency.
|
|
85
|
-
*
|
|
86
|
-
* @example
|
|
87
|
-
* ```typescript
|
|
88
|
-
* // In a route loader
|
|
89
|
-
* async function loader({ params }) {
|
|
90
|
-
* await preloadCollection({
|
|
91
|
-
* id: `users-${params.userId}`,
|
|
92
|
-
* sync: { ... },
|
|
93
|
-
* });
|
|
94
|
-
*
|
|
95
|
-
* return null;
|
|
96
|
-
* }
|
|
97
|
-
* ```
|
|
98
|
-
*
|
|
99
|
-
* @template T - The type of items in the collection
|
|
100
|
-
* @param config - Configuration for the collection, including id and sync
|
|
101
|
-
* @returns Promise that resolves when the initial sync is finished
|
|
102
|
-
*/
|
|
103
|
-
export function preloadCollection<
|
|
104
|
-
T extends object = Record<string, unknown>,
|
|
105
|
-
TKey extends string | number = string | number,
|
|
106
|
-
>(config: CollectionConfig<T, TKey>): Promise<CollectionImpl<T, TKey>> {
|
|
107
|
-
if (!config.id) {
|
|
108
|
-
throw new Error(`The id property is required for preloadCollection`)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// If the collection is already fully loaded, return a resolved promise
|
|
112
|
-
if (
|
|
113
|
-
collectionsStore.has(config.id) &&
|
|
114
|
-
!loadingCollectionResolvers.has(config.id)
|
|
115
|
-
) {
|
|
116
|
-
return Promise.resolve(
|
|
117
|
-
collectionsStore.get(config.id)! as CollectionImpl<T, TKey>
|
|
118
|
-
)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// If the collection is in the process of loading, return its promise
|
|
122
|
-
if (loadingCollectionResolvers.has(config.id)) {
|
|
123
|
-
return loadingCollectionResolvers.get(config.id)!.promise
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Create a new collection instance if it doesn't exist
|
|
127
|
-
if (!collectionsStore.has(config.id)) {
|
|
128
|
-
collectionsStore.set(
|
|
129
|
-
config.id,
|
|
130
|
-
createCollection<T, TKey>({
|
|
131
|
-
id: config.id,
|
|
132
|
-
getKey: config.getKey,
|
|
133
|
-
sync: config.sync,
|
|
134
|
-
schema: config.schema,
|
|
135
|
-
})
|
|
136
|
-
)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const collection = collectionsStore.get(config.id)! as CollectionImpl<T, TKey>
|
|
140
|
-
|
|
141
|
-
// Create a promise that will resolve after the first commit
|
|
142
|
-
let resolveFirstCommit: (value: CollectionImpl<T, TKey>) => void
|
|
143
|
-
const firstCommitPromise = new Promise<CollectionImpl<T, TKey>>((resolve) => {
|
|
144
|
-
resolveFirstCommit = resolve
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
// Store the loading promise first
|
|
148
|
-
loadingCollectionResolvers.set(config.id, {
|
|
149
|
-
promise: firstCommitPromise,
|
|
150
|
-
resolve: resolveFirstCommit!,
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
// Register a one-time listener for the first commit
|
|
154
|
-
collection.onFirstCommit(() => {
|
|
155
|
-
if (!config.id) {
|
|
156
|
-
throw new Error(`The id property is required for preloadCollection`)
|
|
157
|
-
}
|
|
158
|
-
if (loadingCollectionResolvers.has(config.id)) {
|
|
159
|
-
const resolver = loadingCollectionResolvers.get(config.id)!
|
|
160
|
-
loadingCollectionResolvers.delete(config.id)
|
|
161
|
-
resolver.resolve(collection)
|
|
162
|
-
}
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
return firstCommitPromise
|
|
101
|
+
return collection as Collection<
|
|
102
|
+
ResolveType<TExplicit, TSchema, TFallback>,
|
|
103
|
+
TKey,
|
|
104
|
+
TUtils
|
|
105
|
+
>
|
|
166
106
|
}
|
|
167
107
|
|
|
168
108
|
/**
|
|
@@ -184,8 +124,8 @@ export class SchemaValidationError extends Error {
|
|
|
184
124
|
message?: string
|
|
185
125
|
) {
|
|
186
126
|
const defaultMessage = `${type === `insert` ? `Insert` : `Update`} validation failed: ${issues
|
|
187
|
-
.map((issue) => issue.message)
|
|
188
|
-
.join(
|
|
127
|
+
.map((issue) => `\n- ${issue.message} - path: ${issue.path}`)
|
|
128
|
+
.join(``)}`
|
|
189
129
|
|
|
190
130
|
super(message || defaultMessage)
|
|
191
131
|
this.name = `SchemaValidationError`
|
|
@@ -221,7 +161,7 @@ export class CollectionImpl<
|
|
|
221
161
|
|
|
222
162
|
private pendingSyncedTransactions: Array<PendingSyncedTransaction<T>> = []
|
|
223
163
|
private syncedKeys = new Set<TKey>()
|
|
224
|
-
public config: CollectionConfig<T, TKey>
|
|
164
|
+
public config: CollectionConfig<T, TKey, any>
|
|
225
165
|
private hasReceivedFirstCommit = false
|
|
226
166
|
|
|
227
167
|
// Array to store one-time commit listeners
|
|
@@ -244,7 +184,7 @@ export class CollectionImpl<
|
|
|
244
184
|
* @param config - Configuration object for the collection
|
|
245
185
|
* @throws Error if sync config is missing
|
|
246
186
|
*/
|
|
247
|
-
constructor(config: CollectionConfig<T, TKey>) {
|
|
187
|
+
constructor(config: CollectionConfig<T, TKey, any>) {
|
|
248
188
|
// eslint-disable-next-line
|
|
249
189
|
if (!config) {
|
|
250
190
|
throw new Error(`Collection requires a config`)
|
|
@@ -803,7 +743,7 @@ export class CollectionImpl<
|
|
|
803
743
|
}
|
|
804
744
|
|
|
805
745
|
const items = Array.isArray(data) ? data : [data]
|
|
806
|
-
const mutations: Array<PendingMutation<T
|
|
746
|
+
const mutations: Array<PendingMutation<T, `insert`>> = []
|
|
807
747
|
|
|
808
748
|
// Create mutations for each item
|
|
809
749
|
items.forEach((item) => {
|
|
@@ -817,7 +757,7 @@ export class CollectionImpl<
|
|
|
817
757
|
}
|
|
818
758
|
const globalKey = this.generateGlobalKey(key, item)
|
|
819
759
|
|
|
820
|
-
const mutation: PendingMutation<T
|
|
760
|
+
const mutation: PendingMutation<T, `insert`> = {
|
|
821
761
|
mutationId: crypto.randomUUID(),
|
|
822
762
|
original: {},
|
|
823
763
|
modified: validatedData,
|
|
@@ -987,7 +927,7 @@ export class CollectionImpl<
|
|
|
987
927
|
}
|
|
988
928
|
|
|
989
929
|
// Create mutations for each object that has changes
|
|
990
|
-
const mutations: Array<PendingMutation<T
|
|
930
|
+
const mutations: Array<PendingMutation<T, `update`>> = keysArray
|
|
991
931
|
.map((key, index) => {
|
|
992
932
|
const itemChanges = changesArray[index] // User-provided changes for this specific item
|
|
993
933
|
|
|
@@ -1025,9 +965,9 @@ export class CollectionImpl<
|
|
|
1025
965
|
|
|
1026
966
|
return {
|
|
1027
967
|
mutationId: crypto.randomUUID(),
|
|
1028
|
-
original: originalItem
|
|
1029
|
-
modified: modifiedItem
|
|
1030
|
-
changes: validatedUpdatePayload as
|
|
968
|
+
original: originalItem,
|
|
969
|
+
modified: modifiedItem,
|
|
970
|
+
changes: validatedUpdatePayload as Partial<T>,
|
|
1031
971
|
globalKey,
|
|
1032
972
|
key,
|
|
1033
973
|
metadata: config.metadata as unknown,
|
|
@@ -1041,7 +981,7 @@ export class CollectionImpl<
|
|
|
1041
981
|
collection: this,
|
|
1042
982
|
}
|
|
1043
983
|
})
|
|
1044
|
-
.filter(Boolean) as Array<PendingMutation<T
|
|
984
|
+
.filter(Boolean) as Array<PendingMutation<T, `update`>>
|
|
1045
985
|
|
|
1046
986
|
// If no changes were made, return an empty transaction early
|
|
1047
987
|
if (mutations.length === 0) {
|
|
@@ -1117,15 +1057,20 @@ export class CollectionImpl<
|
|
|
1117
1057
|
}
|
|
1118
1058
|
|
|
1119
1059
|
const keysArray = Array.isArray(keys) ? keys : [keys]
|
|
1120
|
-
const mutations: Array<PendingMutation<T
|
|
1060
|
+
const mutations: Array<PendingMutation<T, `delete`>> = []
|
|
1121
1061
|
|
|
1122
1062
|
for (const key of keysArray) {
|
|
1063
|
+
if (!this.has(key)) {
|
|
1064
|
+
throw new Error(
|
|
1065
|
+
`Collection.delete was called with key '${key}' but there is no item in the collection with this key`
|
|
1066
|
+
)
|
|
1067
|
+
}
|
|
1123
1068
|
const globalKey = this.generateGlobalKey(key, this.get(key)!)
|
|
1124
|
-
const mutation: PendingMutation<T
|
|
1069
|
+
const mutation: PendingMutation<T, `delete`> = {
|
|
1125
1070
|
mutationId: crypto.randomUUID(),
|
|
1126
|
-
original: this.get(key)
|
|
1071
|
+
original: this.get(key)!,
|
|
1127
1072
|
modified: this.get(key)!,
|
|
1128
|
-
changes: this.get(key)
|
|
1073
|
+
changes: this.get(key)!,
|
|
1129
1074
|
globalKey,
|
|
1130
1075
|
key,
|
|
1131
1076
|
metadata: config?.metadata as unknown,
|
|
@@ -2,7 +2,7 @@ import { D2, MultiSet, output } from "@electric-sql/d2mini"
|
|
|
2
2
|
import { createCollection } from "../collection.js"
|
|
3
3
|
import { compileQueryPipeline } from "./pipeline-compiler.js"
|
|
4
4
|
import type { Collection } from "../collection.js"
|
|
5
|
-
import type { ChangeMessage, SyncConfig } from "../types.js"
|
|
5
|
+
import type { ChangeMessage, ResolveType, SyncConfig } from "../types.js"
|
|
6
6
|
import type {
|
|
7
7
|
IStreamBuilder,
|
|
8
8
|
MultiSetArray,
|
|
@@ -42,7 +42,13 @@ export class CompiledQuery<TResults extends object = Record<string, unknown>> {
|
|
|
42
42
|
Object.entries(collections).map(([key]) => [key, graph.newInput<any>()])
|
|
43
43
|
)
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
// Use TResults directly to ensure type compatibility
|
|
46
|
+
const sync: SyncConfig<TResults>[`sync`] = ({
|
|
47
|
+
begin,
|
|
48
|
+
write,
|
|
49
|
+
commit,
|
|
50
|
+
collection,
|
|
51
|
+
}) => {
|
|
46
52
|
compileQueryPipeline<IStreamBuilder<[unknown, TResults]>>(
|
|
47
53
|
query,
|
|
48
54
|
inputs
|
|
@@ -69,21 +75,35 @@ export class CompiledQuery<TResults extends object = Record<string, unknown>> {
|
|
|
69
75
|
.forEach((changes, rawKey) => {
|
|
70
76
|
const { deletes, inserts, value } = changes
|
|
71
77
|
const valueWithKey = { ...value, _key: rawKey }
|
|
72
|
-
|
|
78
|
+
|
|
79
|
+
// Simple singular insert.
|
|
80
|
+
if (inserts && deletes === 0) {
|
|
73
81
|
write({
|
|
74
82
|
value: valueWithKey,
|
|
75
83
|
type: `insert`,
|
|
76
84
|
})
|
|
77
|
-
} else if (
|
|
85
|
+
} else if (
|
|
86
|
+
// Insert & update(s) (updates are a delete & insert)
|
|
87
|
+
inserts > deletes ||
|
|
88
|
+
// Just update(s) but the item is already in the collection (so
|
|
89
|
+
// was inserted previously).
|
|
90
|
+
(inserts === deletes &&
|
|
91
|
+
collection.has(valueWithKey._key as string | number))
|
|
92
|
+
) {
|
|
78
93
|
write({
|
|
79
94
|
value: valueWithKey,
|
|
80
95
|
type: `update`,
|
|
81
96
|
})
|
|
97
|
+
// Only delete is left as an option
|
|
82
98
|
} else if (deletes > 0) {
|
|
83
99
|
write({
|
|
84
100
|
value: valueWithKey,
|
|
85
101
|
type: `delete`,
|
|
86
102
|
})
|
|
103
|
+
} else {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`This should never happen ${JSON.stringify(changes)}`
|
|
106
|
+
)
|
|
87
107
|
}
|
|
88
108
|
})
|
|
89
109
|
commit()
|
|
@@ -95,14 +115,30 @@ export class CompiledQuery<TResults extends object = Record<string, unknown>> {
|
|
|
95
115
|
this.graph = graph
|
|
96
116
|
this.inputs = inputs
|
|
97
117
|
this.resultCollection = createCollection<TResults>({
|
|
98
|
-
id: crypto.randomUUID(), // TODO: remove when we don't require any more
|
|
99
118
|
getKey: (val: unknown) => {
|
|
100
119
|
return (val as any)._key
|
|
101
120
|
},
|
|
102
121
|
sync: {
|
|
103
|
-
sync
|
|
122
|
+
sync: sync as unknown as (params: {
|
|
123
|
+
collection: Collection<
|
|
124
|
+
ResolveType<TResults, never, Record<string, unknown>>,
|
|
125
|
+
string | number,
|
|
126
|
+
{}
|
|
127
|
+
>
|
|
128
|
+
begin: () => void
|
|
129
|
+
write: (
|
|
130
|
+
message: Omit<
|
|
131
|
+
ChangeMessage<
|
|
132
|
+
ResolveType<TResults, never, Record<string, unknown>>,
|
|
133
|
+
string | number
|
|
134
|
+
>,
|
|
135
|
+
`key`
|
|
136
|
+
>
|
|
137
|
+
) => void
|
|
138
|
+
commit: () => void
|
|
139
|
+
}) => void,
|
|
104
140
|
},
|
|
105
|
-
})
|
|
141
|
+
}) as unknown as Collection<TResults, string | number, {}>
|
|
106
142
|
}
|
|
107
143
|
|
|
108
144
|
get results() {
|