@tanstack/db 0.0.5 → 0.0.7
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 +86 -27
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +30 -13
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/index.d.cts +1 -1
- package/dist/cjs/query/compiled-query.cjs +1 -1
- package/dist/cjs/query/compiled-query.cjs.map +1 -1
- package/dist/cjs/query/compiled-query.d.cts +1 -1
- package/dist/cjs/query/evaluators.cjs +15 -0
- package/dist/cjs/query/evaluators.cjs.map +1 -1
- package/dist/cjs/query/evaluators.d.cts +5 -1
- package/dist/cjs/query/pipeline-compiler.cjs +2 -2
- package/dist/cjs/query/pipeline-compiler.cjs.map +1 -1
- package/dist/cjs/query/query-builder.cjs +22 -17
- package/dist/cjs/query/query-builder.cjs.map +1 -1
- package/dist/cjs/query/query-builder.d.cts +12 -2
- package/dist/cjs/query/schema.d.cts +11 -5
- package/dist/cjs/query/select.cjs +12 -0
- package/dist/cjs/query/select.cjs.map +1 -1
- package/dist/cjs/transactions.cjs +3 -1
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/types.d.cts +33 -0
- package/dist/esm/collection.d.ts +30 -13
- package/dist/esm/collection.js +87 -28
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/query/compiled-query.d.ts +1 -1
- package/dist/esm/query/compiled-query.js +2 -2
- package/dist/esm/query/compiled-query.js.map +1 -1
- package/dist/esm/query/evaluators.d.ts +5 -1
- package/dist/esm/query/evaluators.js +16 -1
- package/dist/esm/query/evaluators.js.map +1 -1
- package/dist/esm/query/pipeline-compiler.js +3 -3
- package/dist/esm/query/pipeline-compiler.js.map +1 -1
- package/dist/esm/query/query-builder.d.ts +12 -2
- package/dist/esm/query/query-builder.js +22 -17
- package/dist/esm/query/query-builder.js.map +1 -1
- package/dist/esm/query/schema.d.ts +11 -5
- package/dist/esm/query/select.js +12 -0
- package/dist/esm/query/select.js.map +1 -1
- package/dist/esm/transactions.js +3 -1
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +33 -0
- package/package.json +1 -1
- package/src/collection.ts +164 -45
- package/src/index.ts +1 -1
- package/src/query/compiled-query.ts +4 -3
- package/src/query/evaluators.ts +27 -0
- package/src/query/pipeline-compiler.ts +3 -3
- package/src/query/query-builder.ts +40 -23
- package/src/query/schema.ts +17 -5
- package/src/query/select.ts +24 -1
- package/src/transactions.ts +8 -1
- package/src/types.ts +38 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transactions.js","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n PendingMutation,\n TransactionConfig,\n TransactionState,\n} from \"./types\"\n\nfunction generateUUID() {\n // Check if crypto.randomUUID is available (modern browsers and Node.js 15+)\n if (\n typeof crypto !== `undefined` &&\n typeof crypto.randomUUID === `function`\n ) {\n return crypto.randomUUID()\n }\n\n // Fallback implementation for older environments\n return `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`.replace(/[xy]/g, function (c) {\n const r = (Math.random() * 16) | 0\n const v = c === `x` ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n}\n\nconst transactions: Array<Transaction> = []\nlet transactionStack: Array<Transaction> = []\n\nexport function createTransaction(config: TransactionConfig): Transaction {\n if (typeof config.mutationFn === `undefined`) {\n throw `mutationFn is required when creating a transaction`\n }\n\n let transactionId = config.id\n if (!transactionId) {\n transactionId = generateUUID()\n }\n const newTransaction = new Transaction({ ...config, id: transactionId })\n\n transactions.push(newTransaction)\n\n return newTransaction\n}\n\nexport function getActiveTransaction(): Transaction | undefined {\n if (transactionStack.length > 0) {\n return transactionStack.slice(-1)[0]\n } else {\n return undefined\n }\n}\n\nfunction registerTransaction(tx: Transaction) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nexport class Transaction {\n public id: string\n public state: TransactionState\n public mutationFn\n public mutations: Array<PendingMutation<any>>\n public isPersisted: Deferred<Transaction>\n public autoCommit: boolean\n public createdAt: Date\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig) {\n this.id = config.id!\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.metadata = config.metadata ?? {}\n }\n\n setState(newState: TransactionState) {\n this.state = newState\n\n if (newState === `completed` || newState === `failed`) {\n removeFromPendingList(this)\n }\n }\n\n mutate(callback: () => void): Transaction {\n if (this.state !== `pending`) {\n throw `You can no longer call .mutate() as the transaction is no longer pending`\n }\n\n registerTransaction(this)\n try {\n callback()\n } finally {\n unregisterTransaction(this)\n }\n\n if (this.autoCommit) {\n this.commit()\n }\n\n return this\n }\n\n applyMutations(mutations: Array<PendingMutation<any>>): void {\n for (const newMutation of mutations) {\n const existingIndex = this.mutations.findIndex(\n (m) => m.key === newMutation.key\n )\n\n if (existingIndex >= 0) {\n // Replace existing mutation\n this.mutations[existingIndex] = newMutation\n } else {\n // Insert new mutation\n this.mutations.push(newMutation)\n }\n }\n }\n\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction {\n const isSecondaryRollback = config?.isSecondaryRollback ?? false\n if (this.state === `completed`) {\n throw `You can no longer call .rollback() as the transaction is already completed`\n }\n\n this.setState(`failed`)\n\n // See if there's any other transactions w/ mutations on the same ids\n // and roll them back as well.\n if (!isSecondaryRollback) {\n const mutationIds = new Set()\n this.mutations.forEach((m) => mutationIds.add(m.key))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.key)) &&\n t.rollback({ isSecondaryRollback: true })\n }\n }\n\n // Reject the promise\n this.isPersisted.reject(this.error?.error)\n this.touchCollection()\n\n return this\n }\n\n // Tell collection that something has changed with the transaction\n touchCollection(): void {\n const hasCalled = new Set()\n for (const mutation of this.mutations) {\n if (!hasCalled.has(mutation.collection.id)) {\n mutation.collection.transactions.setState((state) => state)\n mutation.collection.commitPendingTransactions()\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n async commit(): Promise<Transaction> {\n if (this.state !== `pending`) {\n throw `You can no longer call .commit() as the transaction is no longer pending`\n }\n\n this.setState(`persisting`)\n\n if (this.mutations.length === 0) {\n this.setState(`completed`)\n }\n\n // Run mutationFn\n try {\n await this.mutationFn({ transaction:
|
|
1
|
+
{"version":3,"file":"transactions.js","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n PendingMutation,\n TransactionConfig,\n TransactionState,\n TransactionWithMutations,\n} from \"./types\"\n\nfunction generateUUID() {\n // Check if crypto.randomUUID is available (modern browsers and Node.js 15+)\n if (\n typeof crypto !== `undefined` &&\n typeof crypto.randomUUID === `function`\n ) {\n return crypto.randomUUID()\n }\n\n // Fallback implementation for older environments\n return `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`.replace(/[xy]/g, function (c) {\n const r = (Math.random() * 16) | 0\n const v = c === `x` ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n}\n\nconst transactions: Array<Transaction> = []\nlet transactionStack: Array<Transaction> = []\n\nexport function createTransaction(config: TransactionConfig): Transaction {\n if (typeof config.mutationFn === `undefined`) {\n throw `mutationFn is required when creating a transaction`\n }\n\n let transactionId = config.id\n if (!transactionId) {\n transactionId = generateUUID()\n }\n const newTransaction = new Transaction({ ...config, id: transactionId })\n\n transactions.push(newTransaction)\n\n return newTransaction\n}\n\nexport function getActiveTransaction(): Transaction | undefined {\n if (transactionStack.length > 0) {\n return transactionStack.slice(-1)[0]\n } else {\n return undefined\n }\n}\n\nfunction registerTransaction(tx: Transaction) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nexport class Transaction {\n public id: string\n public state: TransactionState\n public mutationFn\n public mutations: Array<PendingMutation<any>>\n public isPersisted: Deferred<Transaction>\n public autoCommit: boolean\n public createdAt: Date\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig) {\n this.id = config.id!\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.metadata = config.metadata ?? {}\n }\n\n setState(newState: TransactionState) {\n this.state = newState\n\n if (newState === `completed` || newState === `failed`) {\n removeFromPendingList(this)\n }\n }\n\n mutate(callback: () => void): Transaction {\n if (this.state !== `pending`) {\n throw `You can no longer call .mutate() as the transaction is no longer pending`\n }\n\n registerTransaction(this)\n try {\n callback()\n } finally {\n unregisterTransaction(this)\n }\n\n if (this.autoCommit) {\n this.commit()\n }\n\n return this\n }\n\n applyMutations(mutations: Array<PendingMutation<any>>): void {\n for (const newMutation of mutations) {\n const existingIndex = this.mutations.findIndex(\n (m) => m.key === newMutation.key\n )\n\n if (existingIndex >= 0) {\n // Replace existing mutation\n this.mutations[existingIndex] = newMutation\n } else {\n // Insert new mutation\n this.mutations.push(newMutation)\n }\n }\n }\n\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction {\n const isSecondaryRollback = config?.isSecondaryRollback ?? false\n if (this.state === `completed`) {\n throw `You can no longer call .rollback() as the transaction is already completed`\n }\n\n this.setState(`failed`)\n\n // See if there's any other transactions w/ mutations on the same ids\n // and roll them back as well.\n if (!isSecondaryRollback) {\n const mutationIds = new Set()\n this.mutations.forEach((m) => mutationIds.add(m.key))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.key)) &&\n t.rollback({ isSecondaryRollback: true })\n }\n }\n\n // Reject the promise\n this.isPersisted.reject(this.error?.error)\n this.touchCollection()\n\n return this\n }\n\n // Tell collection that something has changed with the transaction\n touchCollection(): void {\n const hasCalled = new Set()\n for (const mutation of this.mutations) {\n if (!hasCalled.has(mutation.collection.id)) {\n mutation.collection.transactions.setState((state) => state)\n mutation.collection.commitPendingTransactions()\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n async commit(): Promise<Transaction> {\n if (this.state !== `pending`) {\n throw `You can no longer call .commit() as the transaction is no longer pending`\n }\n\n this.setState(`persisting`)\n\n if (this.mutations.length === 0) {\n this.setState(`completed`)\n\n return this\n }\n\n // Run mutationFn\n try {\n // At this point we know there's at least one mutation\n // Use type assertion to tell TypeScript about this guarantee\n const transactionWithMutations =\n this as unknown as TransactionWithMutations\n await this.mutationFn({ transaction: transactionWithMutations })\n\n this.setState(`completed`)\n this.touchCollection()\n\n this.isPersisted.resolve(this)\n } catch (error) {\n // Update transaction with error information\n this.error = {\n message: error instanceof Error ? error.message : String(error),\n error: error instanceof Error ? error : new Error(String(error)),\n }\n\n // rollback the transaction\n return this.rollback()\n }\n\n return this\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -3,6 +3,14 @@ import { Collection } from './collection.js';
|
|
|
3
3
|
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
4
4
|
import { Transaction } from './transactions.js';
|
|
5
5
|
export type TransactionState = `pending` | `persisting` | `completed` | `failed`;
|
|
6
|
+
/**
|
|
7
|
+
* Represents a utility function that can be attached to a collection
|
|
8
|
+
*/
|
|
9
|
+
export type Fn = (...args: Array<any>) => any;
|
|
10
|
+
/**
|
|
11
|
+
* A record of utility functions that can be attached to a collection
|
|
12
|
+
*/
|
|
13
|
+
export type UtilsRecord = Record<string, Fn>;
|
|
6
14
|
/**
|
|
7
15
|
* Represents a pending mutation within a transaction
|
|
8
16
|
* Contains information about the original and modified data, as well as metadata
|
|
@@ -27,6 +35,13 @@ export type MutationFnParams = {
|
|
|
27
35
|
transaction: Transaction;
|
|
28
36
|
};
|
|
29
37
|
export type MutationFn = (params: MutationFnParams) => Promise<any>;
|
|
38
|
+
/**
|
|
39
|
+
* Utility type for a Transaction with at least one mutation
|
|
40
|
+
* This is used internally by the Transaction.commit method
|
|
41
|
+
*/
|
|
42
|
+
export type TransactionWithMutations<T extends object = Record<string, unknown>> = Transaction & {
|
|
43
|
+
mutations: [PendingMutation<T>, ...Array<PendingMutation<T>>];
|
|
44
|
+
};
|
|
30
45
|
export interface TransactionConfig {
|
|
31
46
|
/** Unique identifier for the transaction */
|
|
32
47
|
id?: string;
|
|
@@ -100,6 +115,24 @@ export interface CollectionConfig<T extends object = Record<string, unknown>> {
|
|
|
100
115
|
* getId: (item) => item.uuid
|
|
101
116
|
*/
|
|
102
117
|
getId: (item: T) => any;
|
|
118
|
+
/**
|
|
119
|
+
* Optional asynchronous handler function called before an insert operation
|
|
120
|
+
* @param params Object containing transaction and mutation information
|
|
121
|
+
* @returns Promise resolving to any value
|
|
122
|
+
*/
|
|
123
|
+
onInsert?: MutationFn;
|
|
124
|
+
/**
|
|
125
|
+
* Optional asynchronous handler function called before an update operation
|
|
126
|
+
* @param params Object containing transaction and mutation information
|
|
127
|
+
* @returns Promise resolving to any value
|
|
128
|
+
*/
|
|
129
|
+
onUpdate?: MutationFn;
|
|
130
|
+
/**
|
|
131
|
+
* Optional asynchronous handler function called before a delete operation
|
|
132
|
+
* @param params Object containing transaction and mutation information
|
|
133
|
+
* @returns Promise resolving to any value
|
|
134
|
+
*/
|
|
135
|
+
onDelete?: MutationFn;
|
|
103
136
|
}
|
|
104
137
|
export type ChangesPayload<T extends object = Record<string, unknown>> = Array<ChangeMessage<T>>;
|
|
105
138
|
/**
|
package/package.json
CHANGED
package/src/collection.ts
CHANGED
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
import { Derived, Store, batch } from "@tanstack/store"
|
|
2
2
|
import { withArrayChangeTracking, withChangeTracking } from "./proxy"
|
|
3
|
-
import { getActiveTransaction } from "./transactions"
|
|
3
|
+
import { Transaction, getActiveTransaction } from "./transactions"
|
|
4
4
|
import { SortedMap } from "./SortedMap"
|
|
5
5
|
import type {
|
|
6
6
|
ChangeMessage,
|
|
7
7
|
CollectionConfig,
|
|
8
|
+
Fn,
|
|
8
9
|
InsertConfig,
|
|
9
10
|
OperationConfig,
|
|
10
11
|
OptimisticChangeMessage,
|
|
11
12
|
PendingMutation,
|
|
12
13
|
StandardSchema,
|
|
13
|
-
Transaction,
|
|
14
|
+
Transaction as TransactionType,
|
|
15
|
+
UtilsRecord,
|
|
14
16
|
} from "./types"
|
|
15
17
|
|
|
16
18
|
// Store collections in memory using Tanstack store
|
|
17
|
-
export const collectionsStore = new Store(
|
|
19
|
+
export const collectionsStore = new Store(
|
|
20
|
+
new Map<string, CollectionImpl<any>>()
|
|
21
|
+
)
|
|
18
22
|
|
|
19
23
|
// Map to track loading collections
|
|
20
24
|
|
|
21
25
|
const loadingCollections = new Map<
|
|
22
26
|
string,
|
|
23
|
-
Promise<
|
|
27
|
+
Promise<CollectionImpl<Record<string, unknown>>>
|
|
24
28
|
>()
|
|
25
29
|
|
|
26
30
|
interface PendingSyncedTransaction<T extends object = Record<string, unknown>> {
|
|
@@ -28,17 +32,40 @@ interface PendingSyncedTransaction<T extends object = Record<string, unknown>> {
|
|
|
28
32
|
operations: Array<OptimisticChangeMessage<T>>
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Enhanced Collection interface that includes both data type T and utilities TUtils
|
|
37
|
+
* @template T - The type of items in the collection
|
|
38
|
+
* @template TUtils - The utilities record type
|
|
39
|
+
*/
|
|
40
|
+
export interface Collection<
|
|
41
|
+
T extends object = Record<string, unknown>,
|
|
42
|
+
TUtils extends UtilsRecord = {},
|
|
43
|
+
> extends CollectionImpl<T> {
|
|
44
|
+
readonly utils: TUtils
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
/**
|
|
32
48
|
* Creates a new Collection instance with the given configuration
|
|
33
49
|
*
|
|
34
50
|
* @template T - The type of items in the collection
|
|
35
|
-
* @
|
|
36
|
-
* @
|
|
51
|
+
* @template TUtils - The utilities record type
|
|
52
|
+
* @param options - Collection options with optional utilities
|
|
53
|
+
* @returns A new Collection with utilities exposed both at top level and under .utils
|
|
37
54
|
*/
|
|
38
|
-
export function createCollection<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
export function createCollection<
|
|
56
|
+
T extends object = Record<string, unknown>,
|
|
57
|
+
TUtils extends UtilsRecord = {},
|
|
58
|
+
>(options: CollectionConfig<T> & { utils?: TUtils }): Collection<T, TUtils> {
|
|
59
|
+
const collection = new CollectionImpl<T>(options)
|
|
60
|
+
|
|
61
|
+
// Copy utils to both top level and .utils namespace
|
|
62
|
+
if (options.utils) {
|
|
63
|
+
collection.utils = { ...options.utils }
|
|
64
|
+
} else {
|
|
65
|
+
collection.utils = {} as TUtils
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return collection as Collection<T, TUtils>
|
|
42
69
|
}
|
|
43
70
|
|
|
44
71
|
/**
|
|
@@ -69,7 +96,7 @@ export function createCollection<T extends object = Record<string, unknown>>(
|
|
|
69
96
|
*/
|
|
70
97
|
export function preloadCollection<T extends object = Record<string, unknown>>(
|
|
71
98
|
config: CollectionConfig<T>
|
|
72
|
-
): Promise<
|
|
99
|
+
): Promise<CollectionImpl<T>> {
|
|
73
100
|
if (!config.id) {
|
|
74
101
|
throw new Error(`The id property is required for preloadCollection`)
|
|
75
102
|
}
|
|
@@ -86,7 +113,7 @@ export function preloadCollection<T extends object = Record<string, unknown>>(
|
|
|
86
113
|
|
|
87
114
|
// If the collection is in the process of loading, return its promise
|
|
88
115
|
if (loadingCollections.has(config.id)) {
|
|
89
|
-
return loadingCollections.get(config.id)! as Promise<
|
|
116
|
+
return loadingCollections.get(config.id)! as Promise<CollectionImpl<T>>
|
|
90
117
|
}
|
|
91
118
|
|
|
92
119
|
// Create a new collection instance if it doesn't exist
|
|
@@ -98,7 +125,7 @@ export function preloadCollection<T extends object = Record<string, unknown>>(
|
|
|
98
125
|
}
|
|
99
126
|
next.set(
|
|
100
127
|
config.id,
|
|
101
|
-
|
|
128
|
+
createCollection<T>({
|
|
102
129
|
id: config.id,
|
|
103
130
|
getId: config.getId,
|
|
104
131
|
sync: config.sync,
|
|
@@ -113,9 +140,9 @@ export function preloadCollection<T extends object = Record<string, unknown>>(
|
|
|
113
140
|
|
|
114
141
|
// Create a promise that will resolve after the first commit
|
|
115
142
|
let resolveFirstCommit: () => void
|
|
116
|
-
const firstCommitPromise = new Promise<
|
|
143
|
+
const firstCommitPromise = new Promise<CollectionImpl<T>>((resolve) => {
|
|
117
144
|
resolveFirstCommit = () => {
|
|
118
|
-
resolve(collection)
|
|
145
|
+
resolve(collection as CollectionImpl<T>)
|
|
119
146
|
}
|
|
120
147
|
})
|
|
121
148
|
|
|
@@ -133,7 +160,7 @@ export function preloadCollection<T extends object = Record<string, unknown>>(
|
|
|
133
160
|
// Store the loading promise
|
|
134
161
|
loadingCollections.set(
|
|
135
162
|
config.id,
|
|
136
|
-
firstCommitPromise as Promise<
|
|
163
|
+
firstCommitPromise as Promise<CollectionImpl<Record<string, unknown>>>
|
|
137
164
|
)
|
|
138
165
|
|
|
139
166
|
return firstCommitPromise
|
|
@@ -168,8 +195,13 @@ export class SchemaValidationError extends Error {
|
|
|
168
195
|
}
|
|
169
196
|
}
|
|
170
197
|
|
|
171
|
-
export class
|
|
172
|
-
|
|
198
|
+
export class CollectionImpl<T extends object = Record<string, unknown>> {
|
|
199
|
+
/**
|
|
200
|
+
* Utilities namespace
|
|
201
|
+
* This is populated by createCollection
|
|
202
|
+
*/
|
|
203
|
+
public utils: Record<string, Fn> = {}
|
|
204
|
+
public transactions: Store<SortedMap<string, TransactionType>>
|
|
173
205
|
public optimisticOperations: Derived<Array<OptimisticChangeMessage<T>>>
|
|
174
206
|
public derivedState: Derived<Map<string, T>>
|
|
175
207
|
public derivedArray: Derived<Array<T>>
|
|
@@ -218,7 +250,7 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
218
250
|
}
|
|
219
251
|
|
|
220
252
|
this.transactions = new Store(
|
|
221
|
-
new SortedMap<string,
|
|
253
|
+
new SortedMap<string, TransactionType>(
|
|
222
254
|
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
223
255
|
)
|
|
224
256
|
)
|
|
@@ -604,7 +636,7 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
604
636
|
* Inserts one or more items into the collection
|
|
605
637
|
* @param items - Single item or array of items to insert
|
|
606
638
|
* @param config - Optional configuration including metadata and custom keys
|
|
607
|
-
* @returns A
|
|
639
|
+
* @returns A TransactionType object representing the insert operation(s)
|
|
608
640
|
* @throws {SchemaValidationError} If the data fails schema validation
|
|
609
641
|
* @example
|
|
610
642
|
* // Insert a single item
|
|
@@ -620,9 +652,13 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
620
652
|
* insert({ text: "Buy groceries" }, { key: "grocery-task" })
|
|
621
653
|
*/
|
|
622
654
|
insert = (data: T | Array<T>, config?: InsertConfig) => {
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
655
|
+
const ambientTransaction = getActiveTransaction()
|
|
656
|
+
|
|
657
|
+
// If no ambient transaction exists, check for an onInsert handler early
|
|
658
|
+
if (!ambientTransaction && !this.config.onInsert) {
|
|
659
|
+
throw new Error(
|
|
660
|
+
`Collection.insert called directly (not within an explicit transaction) but no 'onInsert' handler is configured.`
|
|
661
|
+
)
|
|
626
662
|
}
|
|
627
663
|
|
|
628
664
|
const items = Array.isArray(data) ? data : [data]
|
|
@@ -662,14 +698,37 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
662
698
|
mutations.push(mutation)
|
|
663
699
|
})
|
|
664
700
|
|
|
665
|
-
transaction
|
|
701
|
+
// If an ambient transaction exists, use it
|
|
702
|
+
if (ambientTransaction) {
|
|
703
|
+
ambientTransaction.applyMutations(mutations)
|
|
666
704
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
705
|
+
this.transactions.setState((sortedMap) => {
|
|
706
|
+
sortedMap.set(ambientTransaction.id, ambientTransaction)
|
|
707
|
+
return sortedMap
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
return ambientTransaction
|
|
711
|
+
} else {
|
|
712
|
+
// Create a new transaction with a mutation function that calls the onInsert handler
|
|
713
|
+
const directOpTransaction = new Transaction({
|
|
714
|
+
mutationFn: async (params) => {
|
|
715
|
+
// Call the onInsert handler with the transaction
|
|
716
|
+
return this.config.onInsert!(params)
|
|
717
|
+
},
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
// Apply mutations to the new transaction
|
|
721
|
+
directOpTransaction.applyMutations(mutations)
|
|
722
|
+
directOpTransaction.commit()
|
|
723
|
+
|
|
724
|
+
// Add the transaction to the collection's transactions store
|
|
725
|
+
this.transactions.setState((sortedMap) => {
|
|
726
|
+
sortedMap.set(directOpTransaction.id, directOpTransaction)
|
|
727
|
+
return sortedMap
|
|
728
|
+
})
|
|
671
729
|
|
|
672
|
-
|
|
730
|
+
return directOpTransaction
|
|
731
|
+
}
|
|
673
732
|
}
|
|
674
733
|
|
|
675
734
|
/**
|
|
@@ -715,13 +774,13 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
715
774
|
id: unknown,
|
|
716
775
|
configOrCallback: ((draft: TItem) => void) | OperationConfig,
|
|
717
776
|
maybeCallback?: (draft: TItem) => void
|
|
718
|
-
):
|
|
777
|
+
): TransactionType
|
|
719
778
|
|
|
720
779
|
update<TItem extends object = T>(
|
|
721
780
|
ids: Array<unknown>,
|
|
722
781
|
configOrCallback: ((draft: Array<TItem>) => void) | OperationConfig,
|
|
723
782
|
maybeCallback?: (draft: Array<TItem>) => void
|
|
724
|
-
):
|
|
783
|
+
): TransactionType
|
|
725
784
|
|
|
726
785
|
update<TItem extends object = T>(
|
|
727
786
|
ids: unknown | Array<unknown>,
|
|
@@ -732,9 +791,13 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
732
791
|
throw new Error(`The first argument to update is missing`)
|
|
733
792
|
}
|
|
734
793
|
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
794
|
+
const ambientTransaction = getActiveTransaction()
|
|
795
|
+
|
|
796
|
+
// If no ambient transaction exists, check for an onUpdate handler early
|
|
797
|
+
if (!ambientTransaction && !this.config.onUpdate) {
|
|
798
|
+
throw new Error(
|
|
799
|
+
`Collection.update called directly (not within an explicit transaction) but no 'onUpdate' handler is configured.`
|
|
800
|
+
)
|
|
738
801
|
}
|
|
739
802
|
|
|
740
803
|
const isArray = Array.isArray(ids)
|
|
@@ -828,21 +891,46 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
828
891
|
throw new Error(`No changes were made to any of the objects`)
|
|
829
892
|
}
|
|
830
893
|
|
|
831
|
-
transaction
|
|
894
|
+
// If an ambient transaction exists, use it
|
|
895
|
+
if (ambientTransaction) {
|
|
896
|
+
ambientTransaction.applyMutations(mutations)
|
|
897
|
+
|
|
898
|
+
this.transactions.setState((sortedMap) => {
|
|
899
|
+
sortedMap.set(ambientTransaction.id, ambientTransaction)
|
|
900
|
+
return sortedMap
|
|
901
|
+
})
|
|
902
|
+
|
|
903
|
+
return ambientTransaction
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// No need to check for onUpdate handler here as we've already checked at the beginning
|
|
907
|
+
|
|
908
|
+
// Create a new transaction with a mutation function that calls the onUpdate handler
|
|
909
|
+
const directOpTransaction = new Transaction({
|
|
910
|
+
mutationFn: async (transaction) => {
|
|
911
|
+
// Call the onUpdate handler with the transaction
|
|
912
|
+
return this.config.onUpdate!(transaction)
|
|
913
|
+
},
|
|
914
|
+
})
|
|
832
915
|
|
|
916
|
+
// Apply mutations to the new transaction
|
|
917
|
+
directOpTransaction.applyMutations(mutations)
|
|
918
|
+
directOpTransaction.commit()
|
|
919
|
+
|
|
920
|
+
// Add the transaction to the collection's transactions store
|
|
833
921
|
this.transactions.setState((sortedMap) => {
|
|
834
|
-
sortedMap.set(
|
|
922
|
+
sortedMap.set(directOpTransaction.id, directOpTransaction)
|
|
835
923
|
return sortedMap
|
|
836
924
|
})
|
|
837
925
|
|
|
838
|
-
return
|
|
926
|
+
return directOpTransaction
|
|
839
927
|
}
|
|
840
928
|
|
|
841
929
|
/**
|
|
842
930
|
* Deletes one or more items from the collection
|
|
843
931
|
* @param ids - Single ID or array of IDs to delete
|
|
844
932
|
* @param config - Optional configuration including metadata
|
|
845
|
-
* @returns A
|
|
933
|
+
* @returns A TransactionType object representing the delete operation(s)
|
|
846
934
|
* @example
|
|
847
935
|
* // Delete a single item
|
|
848
936
|
* delete("todo-1")
|
|
@@ -853,10 +941,17 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
853
941
|
* // Delete with metadata
|
|
854
942
|
* delete("todo-1", { metadata: { reason: "completed" } })
|
|
855
943
|
*/
|
|
856
|
-
delete = (
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
944
|
+
delete = (
|
|
945
|
+
ids: Array<string> | string,
|
|
946
|
+
config?: OperationConfig
|
|
947
|
+
): TransactionType => {
|
|
948
|
+
const ambientTransaction = getActiveTransaction()
|
|
949
|
+
|
|
950
|
+
// If no ambient transaction exists, check for an onDelete handler early
|
|
951
|
+
if (!ambientTransaction && !this.config.onDelete) {
|
|
952
|
+
throw new Error(
|
|
953
|
+
`Collection.delete called directly (not within an explicit transaction) but no 'onDelete' handler is configured.`
|
|
954
|
+
)
|
|
860
955
|
}
|
|
861
956
|
|
|
862
957
|
const idsArray = (Array.isArray(ids) ? ids : [ids]).map((id) =>
|
|
@@ -885,14 +980,38 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
885
980
|
mutations.push(mutation)
|
|
886
981
|
}
|
|
887
982
|
|
|
888
|
-
transaction
|
|
983
|
+
// If an ambient transaction exists, use it
|
|
984
|
+
if (ambientTransaction) {
|
|
985
|
+
ambientTransaction.applyMutations(mutations)
|
|
986
|
+
|
|
987
|
+
this.transactions.setState((sortedMap) => {
|
|
988
|
+
sortedMap.set(ambientTransaction.id, ambientTransaction)
|
|
989
|
+
return sortedMap
|
|
990
|
+
})
|
|
991
|
+
|
|
992
|
+
return ambientTransaction
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Create a new transaction with a mutation function that calls the onDelete handler
|
|
996
|
+
const directOpTransaction = new Transaction({
|
|
997
|
+
autoCommit: true,
|
|
998
|
+
mutationFn: async (transaction) => {
|
|
999
|
+
// Call the onDelete handler with the transaction
|
|
1000
|
+
return this.config.onDelete!(transaction)
|
|
1001
|
+
},
|
|
1002
|
+
})
|
|
1003
|
+
|
|
1004
|
+
// Apply mutations to the new transaction
|
|
1005
|
+
directOpTransaction.applyMutations(mutations)
|
|
1006
|
+
directOpTransaction.commit()
|
|
889
1007
|
|
|
1008
|
+
// Add the transaction to the collection's transactions store
|
|
890
1009
|
this.transactions.setState((sortedMap) => {
|
|
891
|
-
sortedMap.set(
|
|
1010
|
+
sortedMap.set(directOpTransaction.id, directOpTransaction)
|
|
892
1011
|
return sortedMap
|
|
893
1012
|
})
|
|
894
1013
|
|
|
895
|
-
return
|
|
1014
|
+
return directOpTransaction
|
|
896
1015
|
}
|
|
897
1016
|
|
|
898
1017
|
/**
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { D2, MessageType, MultiSet, output } from "@electric-sql/d2ts"
|
|
2
2
|
import { Effect, batch } from "@tanstack/store"
|
|
3
|
-
import {
|
|
3
|
+
import { createCollection } from "../collection.js"
|
|
4
4
|
import { compileQueryPipeline } from "./pipeline-compiler.js"
|
|
5
|
+
import type { Collection } from "../collection.js"
|
|
5
6
|
import type { ChangeMessage, SyncConfig } from "../types.js"
|
|
6
7
|
import type {
|
|
7
8
|
IStreamBuilder,
|
|
@@ -97,9 +98,9 @@ export class CompiledQuery<TResults extends object = Record<string, unknown>> {
|
|
|
97
98
|
|
|
98
99
|
this.graph = graph
|
|
99
100
|
this.inputs = inputs
|
|
100
|
-
this.resultCollection =
|
|
101
|
+
this.resultCollection = createCollection<TResults>({
|
|
101
102
|
id: crypto.randomUUID(), // TODO: remove when we don't require any more
|
|
102
|
-
getId: (val) => {
|
|
103
|
+
getId: (val: unknown) => {
|
|
103
104
|
return (val as any)._key
|
|
104
105
|
},
|
|
105
106
|
sync: {
|
package/src/query/evaluators.ts
CHANGED
|
@@ -6,9 +6,36 @@ import type {
|
|
|
6
6
|
ConditionOperand,
|
|
7
7
|
LogicalOperator,
|
|
8
8
|
SimpleCondition,
|
|
9
|
+
Where,
|
|
10
|
+
WhereCallback,
|
|
9
11
|
} from "./schema.js"
|
|
10
12
|
import type { NamespacedRow } from "../types.js"
|
|
11
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Evaluates a Where clause (which is always an array of conditions and/or callbacks) against a nested row structure
|
|
16
|
+
*/
|
|
17
|
+
export function evaluateWhereOnNamespacedRow(
|
|
18
|
+
namespacedRow: NamespacedRow,
|
|
19
|
+
where: Where,
|
|
20
|
+
mainTableAlias?: string,
|
|
21
|
+
joinedTableAlias?: string
|
|
22
|
+
): boolean {
|
|
23
|
+
// Where is always an array of conditions and/or callbacks
|
|
24
|
+
// Evaluate all items and combine with AND logic
|
|
25
|
+
return where.every((item) => {
|
|
26
|
+
if (typeof item === `function`) {
|
|
27
|
+
return (item as WhereCallback)(namespacedRow)
|
|
28
|
+
} else {
|
|
29
|
+
return evaluateConditionOnNamespacedRow(
|
|
30
|
+
namespacedRow,
|
|
31
|
+
item as Condition,
|
|
32
|
+
mainTableAlias,
|
|
33
|
+
joinedTableAlias
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
12
39
|
/**
|
|
13
40
|
* Evaluates a condition against a nested row structure
|
|
14
41
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { filter, map } from "@electric-sql/d2ts"
|
|
2
|
-
import {
|
|
2
|
+
import { evaluateWhereOnNamespacedRow } from "./evaluators.js"
|
|
3
3
|
import { processJoinClause } from "./joins.js"
|
|
4
4
|
import { processGroupBy } from "./group-by.js"
|
|
5
5
|
import { processOrderBy } from "./order-by.js"
|
|
@@ -95,7 +95,7 @@ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
|
|
|
95
95
|
if (query.where) {
|
|
96
96
|
pipeline = pipeline.pipe(
|
|
97
97
|
filter(([_key, row]) => {
|
|
98
|
-
const result =
|
|
98
|
+
const result = evaluateWhereOnNamespacedRow(
|
|
99
99
|
row,
|
|
100
100
|
query.where!,
|
|
101
101
|
mainTableAlias
|
|
@@ -117,7 +117,7 @@ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
|
|
|
117
117
|
filter(([_key, row]) => {
|
|
118
118
|
// For HAVING, we're working with the flattened row that contains both
|
|
119
119
|
// the group by keys and the aggregate results directly
|
|
120
|
-
const result =
|
|
120
|
+
const result = evaluateWhereOnNamespacedRow(
|
|
121
121
|
row,
|
|
122
122
|
query.having!,
|
|
123
123
|
mainTableAlias
|