@tanstack/db 0.2.4 → 0.3.0
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 +23 -4
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +35 -41
- package/dist/cjs/local-only.cjs.map +1 -1
- package/dist/cjs/local-only.d.cts +17 -43
- package/dist/cjs/local-storage.cjs +3 -12
- package/dist/cjs/local-storage.cjs.map +1 -1
- package/dist/cjs/local-storage.d.cts +16 -39
- package/dist/cjs/query/builder/types.d.cts +3 -10
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/transactions.cjs +76 -5
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/transactions.d.cts +17 -0
- package/dist/cjs/types.d.cts +10 -31
- package/dist/esm/collection.d.ts +35 -41
- package/dist/esm/collection.js +23 -4
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/local-only.d.ts +17 -43
- package/dist/esm/local-only.js.map +1 -1
- package/dist/esm/local-storage.d.ts +16 -39
- package/dist/esm/local-storage.js +3 -12
- package/dist/esm/local-storage.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +3 -10
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/transactions.d.ts +17 -0
- package/dist/esm/transactions.js +76 -5
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +10 -31
- package/package.json +2 -2
- package/src/collection.ts +148 -196
- package/src/local-only.ts +57 -77
- package/src/local-storage.ts +53 -85
- package/src/query/builder/types.ts +3 -12
- package/src/query/live-query-collection.ts +1 -1
- package/src/transactions.ts +121 -6
- package/src/types.ts +25 -55
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { CollectionImpl } from '../../collection.js';
|
|
2
2
|
import { Aggregate, BasicExpression, Func, OrderByDirection, PropRef, Value } from '../ir.js';
|
|
3
3
|
import { QueryBuilder } from './index.js';
|
|
4
|
-
import { ResolveType } from '../../types.js';
|
|
5
4
|
/**
|
|
6
5
|
* Context - The central state container for query builder operations
|
|
7
6
|
*
|
|
@@ -57,16 +56,10 @@ export type Source = {
|
|
|
57
56
|
/**
|
|
58
57
|
* InferCollectionType - Extracts the TypeScript type from a CollectionImpl
|
|
59
58
|
*
|
|
60
|
-
* This helper ensures we get the same type that
|
|
61
|
-
*
|
|
62
|
-
* consistency between collection creation and query type inference.
|
|
63
|
-
*
|
|
64
|
-
* The complex generic parameters extract:
|
|
65
|
-
* - U: The base document type
|
|
66
|
-
* - TSchema: The schema definition
|
|
67
|
-
* - The resolved type combines these with any transforms
|
|
59
|
+
* This helper ensures we get the same type that was used when creating the collection itself.
|
|
60
|
+
* This can be an explicit type passed by the user or the schema output type.
|
|
68
61
|
*/
|
|
69
|
-
export type InferCollectionType<T> = T extends CollectionImpl<infer
|
|
62
|
+
export type InferCollectionType<T> = T extends CollectionImpl<infer TOutput, any, any, any, any> ? TOutput : never;
|
|
70
63
|
/**
|
|
71
64
|
* SchemaFromSource - Converts a Source definition into a ContextSchema
|
|
72
65
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live-query-collection.cjs","sources":["../../../src/query/live-query-collection.ts"],"sourcesContent":["import { createCollection } from \"../collection.js\"\nimport { CollectionConfigBuilder } from \"./live/collection-config-builder.js\"\nimport type { LiveQueryCollectionConfig } from \"./live/types.js\"\nimport type { InitialQueryBuilder, QueryBuilder } from \"./builder/index.js\"\nimport type { Collection } from \"../collection.js\"\nimport type { CollectionConfig, UtilsRecord } from \"../types.js\"\nimport type { Context, GetResult } from \"./builder/types.js\"\n\n/**\n * Creates live query collection options for use with createCollection\n *\n * @example\n * ```typescript\n * const options = liveQueryCollectionOptions({\n * // id is optional - will auto-generate if not provided\n * query: (q) => q\n * .from({ post: postsCollection })\n * .where(({ post }) => eq(post.published, true))\n * .select(({ post }) => ({\n * id: post.id,\n * title: post.title,\n * content: post.content,\n * })),\n * // getKey is optional - will use stream key if not provided\n * })\n *\n * const collection = createCollection(options)\n * ```\n *\n * @param config - Configuration options for the live query collection\n * @returns Collection options that can be passed to createCollection\n */\nexport function liveQueryCollectionOptions<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n>(\n config: LiveQueryCollectionConfig<TContext, TResult>\n): CollectionConfig<TResult> {\n const collectionConfigBuilder = new CollectionConfigBuilder<\n TContext,\n TResult\n >(config)\n return collectionConfigBuilder.getConfig()\n}\n\n/**\n * Creates a live query collection directly\n *\n * @example\n * ```typescript\n * // Minimal usage - just pass a query function\n * const activeUsers = createLiveQueryCollection(\n * (q) => q\n * .from({ user: usersCollection })\n * .where(({ user }) => eq(user.active, true))\n * .select(({ user }) => ({ id: user.id, name: user.name }))\n * )\n *\n * // Full configuration with custom options\n * const searchResults = createLiveQueryCollection({\n * id: \"search-results\", // Custom ID (auto-generated if omitted)\n * query: (q) => q\n * .from({ post: postsCollection })\n * .where(({ post }) => like(post.title, `%${searchTerm}%`))\n * .select(({ post }) => ({\n * id: post.id,\n * title: post.title,\n * excerpt: post.excerpt,\n * })),\n * getKey: (item) => item.id, // Custom key function (uses stream key if omitted)\n * utils: {\n * updateSearchTerm: (newTerm: string) => {\n * // Custom utility functions\n * }\n * }\n * })\n * ```\n */\n\n// Overload 1: Accept just the query function\nexport function createLiveQueryCollection<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n>(\n query: (q: InitialQueryBuilder) => QueryBuilder<TContext>\n): Collection<TResult, string | number, {}>\n\n// Overload 2: Accept full config object with optional utilities\nexport function createLiveQueryCollection<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n TUtils extends UtilsRecord = {},\n>(\n config: LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils }\n): Collection<TResult, string | number, TUtils>\n\n// Implementation\nexport function createLiveQueryCollection<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n TUtils extends UtilsRecord = {},\n>(\n configOrQuery:\n | (LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils })\n | ((q: InitialQueryBuilder) => QueryBuilder<TContext>)\n): Collection<TResult, string | number, TUtils> {\n // Determine if the argument is a function (query) or a config object\n if (typeof configOrQuery === `function`) {\n // Simple query function case\n const config: LiveQueryCollectionConfig<TContext, TResult> = {\n query: configOrQuery as (\n q: InitialQueryBuilder\n ) => QueryBuilder<TContext>,\n }\n const options = liveQueryCollectionOptions<TContext, TResult>(config)\n return bridgeToCreateCollection(options)\n } else {\n // Config object case\n const config = configOrQuery as LiveQueryCollectionConfig<\n TContext,\n TResult\n > & { utils?: TUtils }\n const options = liveQueryCollectionOptions<TContext, TResult>(config)\n return bridgeToCreateCollection({\n ...options,\n utils: config.utils,\n })\n }\n}\n\n/**\n * Bridge function that handles the type compatibility between query2's TResult\n * and core collection's
|
|
1
|
+
{"version":3,"file":"live-query-collection.cjs","sources":["../../../src/query/live-query-collection.ts"],"sourcesContent":["import { createCollection } from \"../collection.js\"\nimport { CollectionConfigBuilder } from \"./live/collection-config-builder.js\"\nimport type { LiveQueryCollectionConfig } from \"./live/types.js\"\nimport type { InitialQueryBuilder, QueryBuilder } from \"./builder/index.js\"\nimport type { Collection } from \"../collection.js\"\nimport type { CollectionConfig, UtilsRecord } from \"../types.js\"\nimport type { Context, GetResult } from \"./builder/types.js\"\n\n/**\n * Creates live query collection options for use with createCollection\n *\n * @example\n * ```typescript\n * const options = liveQueryCollectionOptions({\n * // id is optional - will auto-generate if not provided\n * query: (q) => q\n * .from({ post: postsCollection })\n * .where(({ post }) => eq(post.published, true))\n * .select(({ post }) => ({\n * id: post.id,\n * title: post.title,\n * content: post.content,\n * })),\n * // getKey is optional - will use stream key if not provided\n * })\n *\n * const collection = createCollection(options)\n * ```\n *\n * @param config - Configuration options for the live query collection\n * @returns Collection options that can be passed to createCollection\n */\nexport function liveQueryCollectionOptions<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n>(\n config: LiveQueryCollectionConfig<TContext, TResult>\n): CollectionConfig<TResult> {\n const collectionConfigBuilder = new CollectionConfigBuilder<\n TContext,\n TResult\n >(config)\n return collectionConfigBuilder.getConfig()\n}\n\n/**\n * Creates a live query collection directly\n *\n * @example\n * ```typescript\n * // Minimal usage - just pass a query function\n * const activeUsers = createLiveQueryCollection(\n * (q) => q\n * .from({ user: usersCollection })\n * .where(({ user }) => eq(user.active, true))\n * .select(({ user }) => ({ id: user.id, name: user.name }))\n * )\n *\n * // Full configuration with custom options\n * const searchResults = createLiveQueryCollection({\n * id: \"search-results\", // Custom ID (auto-generated if omitted)\n * query: (q) => q\n * .from({ post: postsCollection })\n * .where(({ post }) => like(post.title, `%${searchTerm}%`))\n * .select(({ post }) => ({\n * id: post.id,\n * title: post.title,\n * excerpt: post.excerpt,\n * })),\n * getKey: (item) => item.id, // Custom key function (uses stream key if omitted)\n * utils: {\n * updateSearchTerm: (newTerm: string) => {\n * // Custom utility functions\n * }\n * }\n * })\n * ```\n */\n\n// Overload 1: Accept just the query function\nexport function createLiveQueryCollection<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n>(\n query: (q: InitialQueryBuilder) => QueryBuilder<TContext>\n): Collection<TResult, string | number, {}>\n\n// Overload 2: Accept full config object with optional utilities\nexport function createLiveQueryCollection<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n TUtils extends UtilsRecord = {},\n>(\n config: LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils }\n): Collection<TResult, string | number, TUtils>\n\n// Implementation\nexport function createLiveQueryCollection<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n TUtils extends UtilsRecord = {},\n>(\n configOrQuery:\n | (LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils })\n | ((q: InitialQueryBuilder) => QueryBuilder<TContext>)\n): Collection<TResult, string | number, TUtils> {\n // Determine if the argument is a function (query) or a config object\n if (typeof configOrQuery === `function`) {\n // Simple query function case\n const config: LiveQueryCollectionConfig<TContext, TResult> = {\n query: configOrQuery as (\n q: InitialQueryBuilder\n ) => QueryBuilder<TContext>,\n }\n const options = liveQueryCollectionOptions<TContext, TResult>(config)\n return bridgeToCreateCollection(options)\n } else {\n // Config object case\n const config = configOrQuery as LiveQueryCollectionConfig<\n TContext,\n TResult\n > & { utils?: TUtils }\n const options = liveQueryCollectionOptions<TContext, TResult>(config)\n return bridgeToCreateCollection({\n ...options,\n utils: config.utils,\n })\n }\n}\n\n/**\n * Bridge function that handles the type compatibility between query2's TResult\n * and core collection's output type without exposing ugly type assertions to users\n */\nfunction bridgeToCreateCollection<\n TResult extends object,\n TUtils extends UtilsRecord = {},\n>(\n options: CollectionConfig<TResult> & { utils?: TUtils }\n): Collection<TResult, string | number, TUtils> {\n // This is the only place we need a type assertion, hidden from user API\n return createCollection(options as any) as unknown as Collection<\n TResult,\n string | number,\n TUtils\n >\n}\n"],"names":["collectionConfigBuilder","CollectionConfigBuilder","createCollection"],"mappings":";;;;AAgCO,SAAS,2BAId,QAC2B;AAC3B,QAAMA,4BAA0B,IAAIC,wBAAAA,wBAGlC,MAAM;AACR,SAAOD,0BAAwB,UAAA;AACjC;AAsDO,SAAS,0BAKd,eAG8C;AAE9C,MAAI,OAAO,kBAAkB,YAAY;AAEvC,UAAM,SAAuD;AAAA,MAC3D,OAAO;AAAA,IAAA;AAIT,UAAM,UAAU,2BAA8C,MAAM;AACpE,WAAO,yBAAyB,OAAO;AAAA,EACzC,OAAO;AAEL,UAAM,SAAS;AAIf,UAAM,UAAU,2BAA8C,MAAM;AACpE,WAAO,yBAAyB;AAAA,MAC9B,GAAG;AAAA,MACH,OAAO,OAAO;AAAA,IAAA,CACf;AAAA,EACH;AACF;AAMA,SAAS,yBAIP,SAC8C;AAE9C,SAAOE,WAAAA,iBAAiB,OAAc;AAKxC;;;"}
|
|
@@ -5,6 +5,51 @@ const errors = require("./errors.cjs");
|
|
|
5
5
|
const transactions = [];
|
|
6
6
|
let transactionStack = [];
|
|
7
7
|
let sequenceNumber = 0;
|
|
8
|
+
function mergePendingMutations(existing, incoming) {
|
|
9
|
+
switch (`${existing.type}-${incoming.type}`) {
|
|
10
|
+
case `insert-update`: {
|
|
11
|
+
return {
|
|
12
|
+
...existing,
|
|
13
|
+
type: `insert`,
|
|
14
|
+
original: {},
|
|
15
|
+
modified: incoming.modified,
|
|
16
|
+
changes: { ...existing.changes, ...incoming.changes },
|
|
17
|
+
// Keep existing keys (key changes not allowed in updates)
|
|
18
|
+
key: existing.key,
|
|
19
|
+
globalKey: existing.globalKey,
|
|
20
|
+
// Merge metadata (last-write-wins)
|
|
21
|
+
metadata: incoming.metadata ?? existing.metadata,
|
|
22
|
+
syncMetadata: { ...existing.syncMetadata, ...incoming.syncMetadata },
|
|
23
|
+
// Update tracking info
|
|
24
|
+
mutationId: incoming.mutationId,
|
|
25
|
+
updatedAt: incoming.updatedAt
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
case `insert-delete`:
|
|
29
|
+
return null;
|
|
30
|
+
case `update-delete`:
|
|
31
|
+
return incoming;
|
|
32
|
+
case `update-update`: {
|
|
33
|
+
return {
|
|
34
|
+
...incoming,
|
|
35
|
+
// Keep original from first update
|
|
36
|
+
original: existing.original,
|
|
37
|
+
// Union the changes from both updates
|
|
38
|
+
changes: { ...existing.changes, ...incoming.changes },
|
|
39
|
+
// Merge metadata
|
|
40
|
+
metadata: incoming.metadata ?? existing.metadata,
|
|
41
|
+
syncMetadata: { ...existing.syncMetadata, ...incoming.syncMetadata }
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
case `delete-delete`:
|
|
45
|
+
case `insert-insert`:
|
|
46
|
+
return incoming;
|
|
47
|
+
default: {
|
|
48
|
+
const _exhaustive = `${existing.type}-${incoming.type}`;
|
|
49
|
+
throw new Error(`Unhandled mutation combination: ${_exhaustive}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
8
53
|
function createTransaction(config) {
|
|
9
54
|
const newTransaction = new Transaction(config);
|
|
10
55
|
transactions.push(newTransaction);
|
|
@@ -100,17 +145,41 @@ class Transaction {
|
|
|
100
145
|
unregisterTransaction(this);
|
|
101
146
|
}
|
|
102
147
|
if (this.autoCommit) {
|
|
103
|
-
this.commit()
|
|
148
|
+
this.commit().catch(() => {
|
|
149
|
+
});
|
|
104
150
|
}
|
|
105
151
|
return this;
|
|
106
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Apply new mutations to this transaction, intelligently merging with existing mutations
|
|
155
|
+
*
|
|
156
|
+
* When mutations operate on the same item (same globalKey), they are merged according to
|
|
157
|
+
* the following rules:
|
|
158
|
+
*
|
|
159
|
+
* - **insert + update** → insert (merge changes, keep empty original)
|
|
160
|
+
* - **insert + delete** → removed (mutations cancel each other out)
|
|
161
|
+
* - **update + delete** → delete (delete dominates)
|
|
162
|
+
* - **update + update** → update (union changes, keep first original)
|
|
163
|
+
* - **same type** → replace with latest
|
|
164
|
+
*
|
|
165
|
+
* This merging reduces over-the-wire churn and keeps the optimistic local view
|
|
166
|
+
* aligned with user intent.
|
|
167
|
+
*
|
|
168
|
+
* @param mutations - Array of new mutations to apply
|
|
169
|
+
*/
|
|
107
170
|
applyMutations(mutations) {
|
|
108
171
|
for (const newMutation of mutations) {
|
|
109
172
|
const existingIndex = this.mutations.findIndex(
|
|
110
173
|
(m) => m.globalKey === newMutation.globalKey
|
|
111
174
|
);
|
|
112
175
|
if (existingIndex >= 0) {
|
|
113
|
-
this.mutations[existingIndex]
|
|
176
|
+
const existingMutation = this.mutations[existingIndex];
|
|
177
|
+
const mergeResult = mergePendingMutations(existingMutation, newMutation);
|
|
178
|
+
if (mergeResult === null) {
|
|
179
|
+
this.mutations.splice(existingIndex, 1);
|
|
180
|
+
} else {
|
|
181
|
+
this.mutations[existingIndex] = mergeResult;
|
|
182
|
+
}
|
|
114
183
|
} else {
|
|
115
184
|
this.mutations.push(newMutation);
|
|
116
185
|
}
|
|
@@ -242,11 +311,13 @@ class Transaction {
|
|
|
242
311
|
this.touchCollection();
|
|
243
312
|
this.isPersisted.resolve(this);
|
|
244
313
|
} catch (error) {
|
|
314
|
+
const originalError = error instanceof Error ? error : new Error(String(error));
|
|
245
315
|
this.error = {
|
|
246
|
-
message:
|
|
247
|
-
error:
|
|
316
|
+
message: originalError.message,
|
|
317
|
+
error: originalError
|
|
248
318
|
};
|
|
249
|
-
|
|
319
|
+
this.rollback();
|
|
320
|
+
throw originalError;
|
|
250
321
|
}
|
|
251
322
|
return this;
|
|
252
323
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transactions.cjs","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport {\n MissingMutationFunctionError,\n TransactionAlreadyCompletedRollbackError,\n TransactionNotPendingCommitError,\n TransactionNotPendingMutateError,\n} from \"./errors\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n MutationFn,\n PendingMutation,\n TransactionConfig,\n TransactionState,\n TransactionWithMutations,\n} from \"./types\"\n\nconst transactions: Array<Transaction<any>> = []\nlet transactionStack: Array<Transaction<any>> = []\n\nlet sequenceNumber = 0\n\n/**\n * Creates a new transaction for grouping multiple collection operations\n * @param config - Transaction configuration with mutation function\n * @returns A new Transaction instance\n * @example\n * // Basic transaction usage\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Send all mutations to API\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle transaction errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"New item\" })\n * })\n *\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async () => {\n * // API call\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later\n * await tx.commit()\n */\nexport function createTransaction<T extends object = Record<string, unknown>>(\n config: TransactionConfig<T>\n): Transaction<T> {\n const newTransaction = new Transaction<T>(config)\n transactions.push(newTransaction)\n return newTransaction\n}\n\n/**\n * Gets the currently active ambient transaction, if any\n * Used internally by collection operations to join existing transactions\n * @returns The active transaction or undefined if none is active\n * @example\n * // Check if operations will join an ambient transaction\n * const ambientTx = getActiveTransaction()\n * if (ambientTx) {\n * console.log('Operations will join transaction:', ambientTx.id)\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<any>) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction<any>) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction<any>) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nclass Transaction<T extends object = Record<string, unknown>> {\n public id: string\n public state: TransactionState\n public mutationFn: MutationFn<T>\n public mutations: Array<PendingMutation<T>>\n public isPersisted: Deferred<Transaction<T>>\n public autoCommit: boolean\n public createdAt: Date\n public sequenceNumber: number\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig<T>) {\n if (typeof config.mutationFn === `undefined`) {\n throw new MissingMutationFunctionError()\n }\n this.id = config.id ?? crypto.randomUUID()\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred<Transaction<T>>()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.sequenceNumber = sequenceNumber++\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 /**\n * Execute collection operations within this transaction\n * @param callback - Function containing collection operations to group together\n * @returns This transaction for chaining\n * @example\n * // Group multiple operations\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * collection.delete(\"3\")\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle mutate errors\n * try {\n * tx.mutate(() => {\n * collection.insert({ id: \"invalid\" }) // This might throw\n * })\n * } catch (error) {\n * console.log('Mutation failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({ autoCommit: false, mutationFn: async () => {} })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later when ready\n * await tx.commit()\n */\n mutate(callback: () => void): Transaction<T> {\n if (this.state !== `pending`) {\n throw new TransactionNotPendingMutateError()\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.globalKey === newMutation.globalKey\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 /**\n * Rollback the transaction and any conflicting transactions\n * @param config - Configuration for rollback behavior\n * @returns This transaction for chaining\n * @example\n * // Manual rollback\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * // Rollback if needed\n * if (shouldCancel) {\n * tx.rollback()\n * }\n *\n * @example\n * // Handle rollback cascade (automatic)\n * const tx1 = createTransaction({ mutationFn: async () => {} })\n * const tx2 = createTransaction({ mutationFn: async () => {} })\n *\n * tx1.mutate(() => collection.update(\"1\", draft => { draft.value = \"A\" }))\n * tx2.mutate(() => collection.update(\"1\", draft => { draft.value = \"B\" })) // Same item\n *\n * tx1.rollback() // This will also rollback tx2 due to conflict\n *\n * @example\n * // Handle rollback in error scenarios\n * try {\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction was rolled back:', error)\n * // Transaction automatically rolled back on mutation function failure\n * }\n */\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {\n const isSecondaryRollback = config?.isSecondaryRollback ?? false\n if (this.state === `completed`) {\n throw new TransactionAlreadyCompletedRollbackError()\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.globalKey))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.globalKey)) &&\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.onTransactionStateChange()\n\n // Only call commitPendingTransactions if there are pending sync transactions\n if (mutation.collection.pendingSyncedTransactions.length > 0) {\n mutation.collection.commitPendingTransactions()\n }\n\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n /**\n * Commit the transaction and execute the mutation function\n * @returns Promise that resolves to this transaction when complete\n * @example\n * // Manual commit (when autoCommit is false)\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * await tx.commit() // Manually commit\n *\n * @example\n * // Handle commit errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * await tx.commit()\n * } catch (error) {\n * console.log('Commit failed, transaction rolled back:', error)\n * }\n *\n * @example\n * // Check transaction state after commit\n * await tx.commit()\n * console.log(tx.state) // \"completed\" or \"failed\"\n */\n async commit(): Promise<Transaction<T>> {\n if (this.state !== `pending`) {\n throw new TransactionNotPendingCommitError()\n }\n\n this.setState(`persisting`)\n\n if (this.mutations.length === 0) {\n this.setState(`completed`)\n this.isPersisted.resolve(this)\n\n return this\n }\n\n // Run mutationFn\n try {\n // At this point we know there's at least one mutation\n // We've already verified mutations is non-empty, so this cast is safe\n // Use a direct type assertion instead of object spreading to preserve the original type\n await this.mutationFn({\n transaction: this as unknown as TransactionWithMutations<T>,\n })\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 /**\n * Compare two transactions by their createdAt time and sequence number in order\n * to sort them in the order they were created.\n * @param other - The other transaction to compare to\n * @returns -1 if this transaction was created before the other, 1 if it was created after, 0 if they were created at the same time\n */\n compareCreatedAt(other: Transaction<any>): number {\n const createdAtComparison =\n this.createdAt.getTime() - other.createdAt.getTime()\n if (createdAtComparison !== 0) {\n return createdAtComparison\n }\n return this.sequenceNumber - other.sequenceNumber\n }\n}\n\nexport type { Transaction }\n"],"names":["MissingMutationFunctionError","createDeferred","TransactionNotPendingMutateError","TransactionAlreadyCompletedRollbackError","TransactionNotPendingCommitError"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"transactions.cjs","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport {\n MissingMutationFunctionError,\n TransactionAlreadyCompletedRollbackError,\n TransactionNotPendingCommitError,\n TransactionNotPendingMutateError,\n} from \"./errors\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n MutationFn,\n PendingMutation,\n TransactionConfig,\n TransactionState,\n TransactionWithMutations,\n} from \"./types\"\n\nconst transactions: Array<Transaction<any>> = []\nlet transactionStack: Array<Transaction<any>> = []\n\nlet sequenceNumber = 0\n\n/**\n * Merges two pending mutations for the same item within a transaction\n *\n * Merge behavior truth table:\n * - (insert, update) → insert (merge changes, keep empty original)\n * - (insert, delete) → null (cancel both mutations)\n * - (update, delete) → delete (delete dominates)\n * - (update, update) → update (replace with latest, union changes)\n * - (delete, delete) → delete (replace with latest)\n * - (insert, insert) → insert (replace with latest)\n *\n * Note: (delete, update) and (delete, insert) should never occur as the collection\n * layer prevents operations on deleted items within the same transaction.\n *\n * @param existing - The existing mutation in the transaction\n * @param incoming - The new mutation being applied\n * @returns The merged mutation, or null if both should be removed\n */\nfunction mergePendingMutations<T extends object>(\n existing: PendingMutation<T>,\n incoming: PendingMutation<T>\n): PendingMutation<T> | null {\n // Truth table implementation\n switch (`${existing.type}-${incoming.type}` as const) {\n case `insert-update`: {\n // Update after insert: keep as insert but merge changes\n // For insert-update, the key should remain the same since collections don't allow key changes\n return {\n ...existing,\n type: `insert` as const,\n original: {},\n modified: incoming.modified,\n changes: { ...existing.changes, ...incoming.changes },\n // Keep existing keys (key changes not allowed in updates)\n key: existing.key,\n globalKey: existing.globalKey,\n // Merge metadata (last-write-wins)\n metadata: incoming.metadata ?? existing.metadata,\n syncMetadata: { ...existing.syncMetadata, ...incoming.syncMetadata },\n // Update tracking info\n mutationId: incoming.mutationId,\n updatedAt: incoming.updatedAt,\n }\n }\n\n case `insert-delete`:\n // Delete after insert: cancel both mutations\n return null\n\n case `update-delete`:\n // Delete after update: delete dominates\n return incoming\n\n case `update-update`: {\n // Update after update: replace with latest, union changes\n return {\n ...incoming,\n // Keep original from first update\n original: existing.original,\n // Union the changes from both updates\n changes: { ...existing.changes, ...incoming.changes },\n // Merge metadata\n metadata: incoming.metadata ?? existing.metadata,\n syncMetadata: { ...existing.syncMetadata, ...incoming.syncMetadata },\n }\n }\n\n case `delete-delete`:\n case `insert-insert`:\n // Same type: replace with latest\n return incoming\n\n default: {\n // Exhaustiveness check\n const _exhaustive: never = `${existing.type}-${incoming.type}` as never\n throw new Error(`Unhandled mutation combination: ${_exhaustive}`)\n }\n }\n}\n\n/**\n * Creates a new transaction for grouping multiple collection operations\n * @param config - Transaction configuration with mutation function\n * @returns A new Transaction instance\n * @example\n * // Basic transaction usage\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Send all mutations to API\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle transaction errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"New item\" })\n * })\n *\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async () => {\n * // API call\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later\n * await tx.commit()\n */\nexport function createTransaction<T extends object = Record<string, unknown>>(\n config: TransactionConfig<T>\n): Transaction<T> {\n const newTransaction = new Transaction<T>(config)\n transactions.push(newTransaction)\n return newTransaction\n}\n\n/**\n * Gets the currently active ambient transaction, if any\n * Used internally by collection operations to join existing transactions\n * @returns The active transaction or undefined if none is active\n * @example\n * // Check if operations will join an ambient transaction\n * const ambientTx = getActiveTransaction()\n * if (ambientTx) {\n * console.log('Operations will join transaction:', ambientTx.id)\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<any>) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction<any>) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction<any>) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nclass Transaction<T extends object = Record<string, unknown>> {\n public id: string\n public state: TransactionState\n public mutationFn: MutationFn<T>\n public mutations: Array<PendingMutation<T>>\n public isPersisted: Deferred<Transaction<T>>\n public autoCommit: boolean\n public createdAt: Date\n public sequenceNumber: number\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig<T>) {\n if (typeof config.mutationFn === `undefined`) {\n throw new MissingMutationFunctionError()\n }\n this.id = config.id ?? crypto.randomUUID()\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred<Transaction<T>>()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.sequenceNumber = sequenceNumber++\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 /**\n * Execute collection operations within this transaction\n * @param callback - Function containing collection operations to group together\n * @returns This transaction for chaining\n * @example\n * // Group multiple operations\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * collection.delete(\"3\")\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle mutate errors\n * try {\n * tx.mutate(() => {\n * collection.insert({ id: \"invalid\" }) // This might throw\n * })\n * } catch (error) {\n * console.log('Mutation failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({ autoCommit: false, mutationFn: async () => {} })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later when ready\n * await tx.commit()\n */\n mutate(callback: () => void): Transaction<T> {\n if (this.state !== `pending`) {\n throw new TransactionNotPendingMutateError()\n }\n\n registerTransaction(this)\n try {\n callback()\n } finally {\n unregisterTransaction(this)\n }\n\n if (this.autoCommit) {\n this.commit().catch(() => {\n // Errors from autoCommit are handled via isPersisted.promise\n // This catch prevents unhandled promise rejections\n })\n }\n\n return this\n }\n\n /**\n * Apply new mutations to this transaction, intelligently merging with existing mutations\n *\n * When mutations operate on the same item (same globalKey), they are merged according to\n * the following rules:\n *\n * - **insert + update** → insert (merge changes, keep empty original)\n * - **insert + delete** → removed (mutations cancel each other out)\n * - **update + delete** → delete (delete dominates)\n * - **update + update** → update (union changes, keep first original)\n * - **same type** → replace with latest\n *\n * This merging reduces over-the-wire churn and keeps the optimistic local view\n * aligned with user intent.\n *\n * @param mutations - Array of new mutations to apply\n */\n applyMutations(mutations: Array<PendingMutation<any>>): void {\n for (const newMutation of mutations) {\n const existingIndex = this.mutations.findIndex(\n (m) => m.globalKey === newMutation.globalKey\n )\n\n if (existingIndex >= 0) {\n const existingMutation = this.mutations[existingIndex]!\n const mergeResult = mergePendingMutations(existingMutation, newMutation)\n\n if (mergeResult === null) {\n // Remove the mutation (e.g., delete after insert cancels both)\n this.mutations.splice(existingIndex, 1)\n } else {\n // Replace with merged mutation\n this.mutations[existingIndex] = mergeResult\n }\n } else {\n // Insert new mutation\n this.mutations.push(newMutation)\n }\n }\n }\n\n /**\n * Rollback the transaction and any conflicting transactions\n * @param config - Configuration for rollback behavior\n * @returns This transaction for chaining\n * @example\n * // Manual rollback\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * // Rollback if needed\n * if (shouldCancel) {\n * tx.rollback()\n * }\n *\n * @example\n * // Handle rollback cascade (automatic)\n * const tx1 = createTransaction({ mutationFn: async () => {} })\n * const tx2 = createTransaction({ mutationFn: async () => {} })\n *\n * tx1.mutate(() => collection.update(\"1\", draft => { draft.value = \"A\" }))\n * tx2.mutate(() => collection.update(\"1\", draft => { draft.value = \"B\" })) // Same item\n *\n * tx1.rollback() // This will also rollback tx2 due to conflict\n *\n * @example\n * // Handle rollback in error scenarios\n * try {\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction was rolled back:', error)\n * // Transaction automatically rolled back on mutation function failure\n * }\n */\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {\n const isSecondaryRollback = config?.isSecondaryRollback ?? false\n if (this.state === `completed`) {\n throw new TransactionAlreadyCompletedRollbackError()\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.globalKey))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.globalKey)) &&\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.onTransactionStateChange()\n\n // Only call commitPendingTransactions if there are pending sync transactions\n if (mutation.collection.pendingSyncedTransactions.length > 0) {\n mutation.collection.commitPendingTransactions()\n }\n\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n /**\n * Commit the transaction and execute the mutation function\n * @returns Promise that resolves to this transaction when complete\n * @example\n * // Manual commit (when autoCommit is false)\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * await tx.commit() // Manually commit\n *\n * @example\n * // Handle commit errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * await tx.commit()\n * } catch (error) {\n * console.log('Commit failed, transaction rolled back:', error)\n * }\n *\n * @example\n * // Check transaction state after commit\n * await tx.commit()\n * console.log(tx.state) // \"completed\" or \"failed\"\n */\n async commit(): Promise<Transaction<T>> {\n if (this.state !== `pending`) {\n throw new TransactionNotPendingCommitError()\n }\n\n this.setState(`persisting`)\n\n if (this.mutations.length === 0) {\n this.setState(`completed`)\n this.isPersisted.resolve(this)\n\n return this\n }\n\n // Run mutationFn\n try {\n // At this point we know there's at least one mutation\n // We've already verified mutations is non-empty, so this cast is safe\n // Use a direct type assertion instead of object spreading to preserve the original type\n await this.mutationFn({\n transaction: this as unknown as TransactionWithMutations<T>,\n })\n\n this.setState(`completed`)\n this.touchCollection()\n\n this.isPersisted.resolve(this)\n } catch (error) {\n // Preserve the original error for rethrowing\n const originalError =\n error instanceof Error ? error : new Error(String(error))\n\n // Update transaction with error information\n this.error = {\n message: originalError.message,\n error: originalError,\n }\n\n // rollback the transaction\n this.rollback()\n\n // Re-throw the original error to preserve identity and stack\n throw originalError\n }\n\n return this\n }\n\n /**\n * Compare two transactions by their createdAt time and sequence number in order\n * to sort them in the order they were created.\n * @param other - The other transaction to compare to\n * @returns -1 if this transaction was created before the other, 1 if it was created after, 0 if they were created at the same time\n */\n compareCreatedAt(other: Transaction<any>): number {\n const createdAtComparison =\n this.createdAt.getTime() - other.createdAt.getTime()\n if (createdAtComparison !== 0) {\n return createdAtComparison\n }\n return this.sequenceNumber - other.sequenceNumber\n }\n}\n\nexport type { Transaction }\n"],"names":["MissingMutationFunctionError","createDeferred","TransactionNotPendingMutateError","TransactionAlreadyCompletedRollbackError","TransactionNotPendingCommitError"],"mappings":";;;;AAgBA,MAAM,eAAwC,CAAA;AAC9C,IAAI,mBAA4C,CAAA;AAEhD,IAAI,iBAAiB;AAoBrB,SAAS,sBACP,UACA,UAC2B;AAE3B,UAAQ,GAAG,SAAS,IAAI,IAAI,SAAS,IAAI,IAAA;AAAA,IACvC,KAAK,iBAAiB;AAGpB,aAAO;AAAA,QACL,GAAG;AAAA,QACH,MAAM;AAAA,QACN,UAAU,CAAA;AAAA,QACV,UAAU,SAAS;AAAA,QACnB,SAAS,EAAE,GAAG,SAAS,SAAS,GAAG,SAAS,QAAA;AAAA;AAAA,QAE5C,KAAK,SAAS;AAAA,QACd,WAAW,SAAS;AAAA;AAAA,QAEpB,UAAU,SAAS,YAAY,SAAS;AAAA,QACxC,cAAc,EAAE,GAAG,SAAS,cAAc,GAAG,SAAS,aAAA;AAAA;AAAA,QAEtD,YAAY,SAAS;AAAA,QACrB,WAAW,SAAS;AAAA,MAAA;AAAA,IAExB;AAAA,IAEA,KAAK;AAEH,aAAO;AAAA,IAET,KAAK;AAEH,aAAO;AAAA,IAET,KAAK,iBAAiB;AAEpB,aAAO;AAAA,QACL,GAAG;AAAA;AAAA,QAEH,UAAU,SAAS;AAAA;AAAA,QAEnB,SAAS,EAAE,GAAG,SAAS,SAAS,GAAG,SAAS,QAAA;AAAA;AAAA,QAE5C,UAAU,SAAS,YAAY,SAAS;AAAA,QACxC,cAAc,EAAE,GAAG,SAAS,cAAc,GAAG,SAAS,aAAA;AAAA,MAAa;AAAA,IAEvE;AAAA,IAEA,KAAK;AAAA,IACL,KAAK;AAEH,aAAO;AAAA,IAET,SAAS;AAEP,YAAM,cAAqB,GAAG,SAAS,IAAI,IAAI,SAAS,IAAI;AAC5D,YAAM,IAAI,MAAM,mCAAmC,WAAW,EAAE;AAAA,IAClE;AAAA,EAAA;AAEJ;AAsDO,SAAS,kBACd,QACgB;AAChB,QAAM,iBAAiB,IAAI,YAAe,MAAM;AAChD,eAAa,KAAK,cAAc;AAChC,SAAO;AACT;AAaO,SAAS,uBAAgD;AAC9D,MAAI,iBAAiB,SAAS,GAAG;AAC/B,WAAO,iBAAiB,MAAM,EAAE,EAAE,CAAC;AAAA,EACrC,OAAO;AACL,WAAO;AAAA,EACT;AACF;AAEA,SAAS,oBAAoB,IAAsB;AACjD,mBAAiB,KAAK,EAAE;AAC1B;AAEA,SAAS,sBAAsB,IAAsB;AACnD,qBAAmB,iBAAiB,OAAO,CAAC,MAAM,EAAE,OAAO,GAAG,EAAE;AAClE;AAEA,SAAS,sBAAsB,IAAsB;AACnD,QAAM,QAAQ,aAAa,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,EAAE;AAC1D,MAAI,UAAU,IAAI;AAChB,iBAAa,OAAO,OAAO,CAAC;AAAA,EAC9B;AACF;AAEA,MAAM,YAAwD;AAAA,EAe5D,YAAY,QAA8B;AACxC,QAAI,OAAO,OAAO,eAAe,aAAa;AAC5C,YAAM,IAAIA,OAAAA,6BAAA;AAAA,IACZ;AACA,SAAK,KAAK,OAAO,MAAM,OAAO,WAAA;AAC9B,SAAK,aAAa,OAAO;AACzB,SAAK,QAAQ;AACb,SAAK,YAAY,CAAA;AACjB,SAAK,cAAcC,wBAAA;AACnB,SAAK,aAAa,OAAO,cAAc;AACvC,SAAK,gCAAgB,KAAA;AACrB,SAAK,iBAAiB;AACtB,SAAK,WAAW,OAAO,YAAY,CAAA;AAAA,EACrC;AAAA,EAEA,SAAS,UAA4B;AACnC,SAAK,QAAQ;AAEb,QAAI,aAAa,eAAe,aAAa,UAAU;AACrD,4BAAsB,IAAI;AAAA,IAC5B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyCA,OAAO,UAAsC;AAC3C,QAAI,KAAK,UAAU,WAAW;AAC5B,YAAM,IAAIC,OAAAA,iCAAA;AAAA,IACZ;AAEA,wBAAoB,IAAI;AACxB,QAAI;AACF,eAAA;AAAA,IACF,UAAA;AACE,4BAAsB,IAAI;AAAA,IAC5B;AAEA,QAAI,KAAK,YAAY;AACnB,WAAK,SAAS,MAAM,MAAM;AAAA,MAG1B,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBA,eAAe,WAA8C;AAC3D,eAAW,eAAe,WAAW;AACnC,YAAM,gBAAgB,KAAK,UAAU;AAAA,QACnC,CAAC,MAAM,EAAE,cAAc,YAAY;AAAA,MAAA;AAGrC,UAAI,iBAAiB,GAAG;AACtB,cAAM,mBAAmB,KAAK,UAAU,aAAa;AACrD,cAAM,cAAc,sBAAsB,kBAAkB,WAAW;AAEvE,YAAI,gBAAgB,MAAM;AAExB,eAAK,UAAU,OAAO,eAAe,CAAC;AAAA,QACxC,OAAO;AAEL,eAAK,UAAU,aAAa,IAAI;AAAA,QAClC;AAAA,MACF,OAAO;AAEL,aAAK,UAAU,KAAK,WAAW;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAwCA,SAAS,QAA4D;;AACnE,UAAM,uBAAsB,iCAAQ,wBAAuB;AAC3D,QAAI,KAAK,UAAU,aAAa;AAC9B,YAAM,IAAIC,OAAAA,yCAAA;AAAA,IACZ;AAEA,SAAK,SAAS,QAAQ;AAItB,QAAI,CAAC,qBAAqB;AACxB,YAAM,kCAAkB,IAAA;AACxB,WAAK,UAAU,QAAQ,CAAC,MAAM,YAAY,IAAI,EAAE,SAAS,CAAC;AAC1D,iBAAW,KAAK,cAAc;AAC5B,UAAE,UAAU,aACV,EAAE,UAAU,KAAK,CAAC,MAAM,YAAY,IAAI,EAAE,SAAS,CAAC,KACpD,EAAE,SAAS,EAAE,qBAAqB,MAAM;AAAA,MAC5C;AAAA,IACF;AAGA,SAAK,YAAY,QAAO,UAAK,UAAL,mBAAY,KAAK;AACzC,SAAK,gBAAA;AAEL,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,kBAAwB;AACtB,UAAM,gCAAgB,IAAA;AACtB,eAAW,YAAY,KAAK,WAAW;AACrC,UAAI,CAAC,UAAU,IAAI,SAAS,WAAW,EAAE,GAAG;AAC1C,iBAAS,WAAW,yBAAA;AAGpB,YAAI,SAAS,WAAW,0BAA0B,SAAS,GAAG;AAC5D,mBAAS,WAAW,0BAAA;AAAA,QACtB;AAEA,kBAAU,IAAI,SAAS,WAAW,EAAE;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyCA,MAAM,SAAkC;AACtC,QAAI,KAAK,UAAU,WAAW;AAC5B,YAAM,IAAIC,OAAAA,iCAAA;AAAA,IACZ;AAEA,SAAK,SAAS,YAAY;AAE1B,QAAI,KAAK,UAAU,WAAW,GAAG;AAC/B,WAAK,SAAS,WAAW;AACzB,WAAK,YAAY,QAAQ,IAAI;AAE7B,aAAO;AAAA,IACT;AAGA,QAAI;AAIF,YAAM,KAAK,WAAW;AAAA,QACpB,aAAa;AAAA,MAAA,CACd;AAED,WAAK,SAAS,WAAW;AACzB,WAAK,gBAAA;AAEL,WAAK,YAAY,QAAQ,IAAI;AAAA,IAC/B,SAAS,OAAO;AAEd,YAAM,gBACJ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAG1D,WAAK,QAAQ;AAAA,QACX,SAAS,cAAc;AAAA,QACvB,OAAO;AAAA,MAAA;AAIT,WAAK,SAAA;AAGL,YAAM;AAAA,IACR;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,iBAAiB,OAAiC;AAChD,UAAM,sBACJ,KAAK,UAAU,YAAY,MAAM,UAAU,QAAA;AAC7C,QAAI,wBAAwB,GAAG;AAC7B,aAAO;AAAA,IACT;AACA,WAAO,KAAK,iBAAiB,MAAM;AAAA,EACrC;AACF;;;"}
|
|
@@ -121,6 +121,23 @@ declare class Transaction<T extends object = Record<string, unknown>> {
|
|
|
121
121
|
* await tx.commit()
|
|
122
122
|
*/
|
|
123
123
|
mutate(callback: () => void): Transaction<T>;
|
|
124
|
+
/**
|
|
125
|
+
* Apply new mutations to this transaction, intelligently merging with existing mutations
|
|
126
|
+
*
|
|
127
|
+
* When mutations operate on the same item (same globalKey), they are merged according to
|
|
128
|
+
* the following rules:
|
|
129
|
+
*
|
|
130
|
+
* - **insert + update** → insert (merge changes, keep empty original)
|
|
131
|
+
* - **insert + delete** → removed (mutations cancel each other out)
|
|
132
|
+
* - **update + delete** → delete (delete dominates)
|
|
133
|
+
* - **update + update** → update (union changes, keep first original)
|
|
134
|
+
* - **same type** → replace with latest
|
|
135
|
+
*
|
|
136
|
+
* This merging reduces over-the-wire churn and keeps the optimistic local view
|
|
137
|
+
* aligned with user intent.
|
|
138
|
+
*
|
|
139
|
+
* @param mutations - Array of new mutations to apply
|
|
140
|
+
*/
|
|
124
141
|
applyMutations(mutations: Array<PendingMutation<any>>): void;
|
|
125
142
|
/**
|
|
126
143
|
* Rollback the transaction and any conflicting transactions
|
package/dist/cjs/types.d.cts
CHANGED
|
@@ -16,29 +16,6 @@ export type InferSchemaOutput<T> = T extends StandardSchemaV1 ? StandardSchemaV1
|
|
|
16
16
|
* @internal This is used for collection insert type inference
|
|
17
17
|
*/
|
|
18
18
|
export type InferSchemaInput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferInput<T> extends object ? StandardSchemaV1.InferInput<T> : Record<string, unknown> : Record<string, unknown>;
|
|
19
|
-
/**
|
|
20
|
-
* Helper type to determine the insert input type
|
|
21
|
-
* This takes the raw generics (TExplicit, TSchema, TFallback) instead of the resolved T.
|
|
22
|
-
*
|
|
23
|
-
* Priority:
|
|
24
|
-
* 1. Explicit generic TExplicit (if not 'unknown')
|
|
25
|
-
* 2. Schema input type (if schema provided)
|
|
26
|
-
* 3. Fallback type TFallback
|
|
27
|
-
*
|
|
28
|
-
* @internal This is used for collection insert type inference
|
|
29
|
-
*/
|
|
30
|
-
export type ResolveInsertInput<TExplicit = unknown, TSchema extends StandardSchemaV1 = never, TFallback extends object = Record<string, unknown>> = unknown extends TExplicit ? [TSchema] extends [never] ? TFallback : InferSchemaInput<TSchema> : TExplicit extends object ? TExplicit : Record<string, unknown>;
|
|
31
|
-
/**
|
|
32
|
-
* Helper type to determine the final type based on priority:
|
|
33
|
-
* 1. Explicit generic TExplicit (if not 'unknown')
|
|
34
|
-
* 2. Schema output type (if schema provided)
|
|
35
|
-
* 3. Fallback type TFallback
|
|
36
|
-
*
|
|
37
|
-
* @remarks
|
|
38
|
-
* This type is used internally to resolve the collection item type based on the provided generics and schema.
|
|
39
|
-
* Users should not need to use this type directly, but understanding the priority order helps when defining collections.
|
|
40
|
-
*/
|
|
41
|
-
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>;
|
|
42
19
|
export type TransactionState = `pending` | `persisting` | `completed` | `failed`;
|
|
43
20
|
/**
|
|
44
21
|
* Represents a utility function that can be attached to a collection
|
|
@@ -187,9 +164,9 @@ export type DeleteMutationFnParams<T extends object = Record<string, unknown>, T
|
|
|
187
164
|
transaction: TransactionWithMutations<T, `delete`>;
|
|
188
165
|
collection: Collection<T, TKey, TUtils>;
|
|
189
166
|
};
|
|
190
|
-
export type InsertMutationFn<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = Record<string, Fn
|
|
191
|
-
export type UpdateMutationFn<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = Record<string, Fn
|
|
192
|
-
export type DeleteMutationFn<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = Record<string, Fn
|
|
167
|
+
export type InsertMutationFn<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = Record<string, Fn>, TReturn = any> = (params: InsertMutationFnParams<T, TKey, TUtils>) => Promise<TReturn>;
|
|
168
|
+
export type UpdateMutationFn<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = Record<string, Fn>, TReturn = any> = (params: UpdateMutationFnParams<T, TKey, TUtils>) => Promise<TReturn>;
|
|
169
|
+
export type DeleteMutationFn<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = Record<string, Fn>, TReturn = any> = (params: DeleteMutationFnParams<T, TKey, TUtils>) => Promise<TReturn>;
|
|
193
170
|
/**
|
|
194
171
|
* Collection status values for lifecycle management
|
|
195
172
|
* @example
|
|
@@ -218,9 +195,8 @@ export type CollectionStatus =
|
|
|
218
195
|
| `error`
|
|
219
196
|
/** Collection has been cleaned up and resources freed */
|
|
220
197
|
| `cleaned-up`;
|
|
221
|
-
export interface
|
|
198
|
+
export interface BaseCollectionConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = never, TUtils extends UtilsRecord = Record<string, Fn>, TReturn = any> {
|
|
222
199
|
id?: string;
|
|
223
|
-
sync: SyncConfig<T, TKey>;
|
|
224
200
|
schema?: TSchema;
|
|
225
201
|
/**
|
|
226
202
|
* Function to extract the ID from an object
|
|
@@ -303,7 +279,7 @@ export interface CollectionConfig<T extends object = Record<string, unknown>, TK
|
|
|
303
279
|
* })
|
|
304
280
|
* }
|
|
305
281
|
*/
|
|
306
|
-
onInsert?: InsertMutationFn<
|
|
282
|
+
onInsert?: InsertMutationFn<T, TKey, TUtils, TReturn>;
|
|
307
283
|
/**
|
|
308
284
|
* Optional asynchronous handler function called before an update operation
|
|
309
285
|
* @param params Object containing transaction and collection information
|
|
@@ -346,7 +322,7 @@ export interface CollectionConfig<T extends object = Record<string, unknown>, TK
|
|
|
346
322
|
* }
|
|
347
323
|
* }
|
|
348
324
|
*/
|
|
349
|
-
onUpdate?: UpdateMutationFn<T, TKey>;
|
|
325
|
+
onUpdate?: UpdateMutationFn<T, TKey, TUtils, TReturn>;
|
|
350
326
|
/**
|
|
351
327
|
* Optional asynchronous handler function called before a delete operation
|
|
352
328
|
* @param params Object containing transaction and collection information
|
|
@@ -389,7 +365,10 @@ export interface CollectionConfig<T extends object = Record<string, unknown>, TK
|
|
|
389
365
|
* }
|
|
390
366
|
* }
|
|
391
367
|
*/
|
|
392
|
-
onDelete?: DeleteMutationFn<T, TKey>;
|
|
368
|
+
onDelete?: DeleteMutationFn<T, TKey, TUtils, TReturn>;
|
|
369
|
+
}
|
|
370
|
+
export interface CollectionConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = never> extends BaseCollectionConfig<T, TKey, TSchema> {
|
|
371
|
+
sync: SyncConfig<T, TKey>;
|
|
393
372
|
}
|
|
394
373
|
export type ChangesPayload<T extends object = Record<string, unknown>> = Array<ChangeMessage<T>>;
|
|
395
374
|
/**
|
package/dist/esm/collection.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { IndexProxy } from './indexes/lazy-index.js';
|
|
|
4
4
|
import { Transaction } from './transactions.js';
|
|
5
5
|
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
6
6
|
import { SingleRowRefProxy } from './query/builder/ref-proxy.js';
|
|
7
|
-
import { ChangeListener, ChangeMessage, CollectionConfig, CollectionStatus, CurrentStateAsChangesOptions, Fn,
|
|
7
|
+
import { ChangeListener, ChangeMessage, CollectionConfig, CollectionStatus, CurrentStateAsChangesOptions, Fn, InferSchemaInput, InferSchemaOutput, InsertConfig, OperationConfig, OptimisticChangeMessage, SubscribeChangesOptions, Transaction as TransactionType, UtilsRecord, WritableDeep } from './types.js';
|
|
8
8
|
import { IndexOptions } from './indexes/index-options.js';
|
|
9
9
|
import { BaseIndex, IndexResolver } from './indexes/base-index.js';
|
|
10
10
|
interface PendingSyncedTransaction<T extends object = Record<string, unknown>> {
|
|
@@ -26,11 +26,9 @@ export interface Collection<T extends object = Record<string, unknown>, TKey ext
|
|
|
26
26
|
/**
|
|
27
27
|
* Creates a new Collection instance with the given configuration
|
|
28
28
|
*
|
|
29
|
-
* @template
|
|
29
|
+
* @template T - The schema type if a schema is provided, otherwise the type of items in the collection
|
|
30
30
|
* @template TKey - The type of the key for the collection
|
|
31
31
|
* @template TUtils - The utilities record type
|
|
32
|
-
* @template TSchema - The schema type for validation and type inference (second priority)
|
|
33
|
-
* @template TFallback - The fallback type if no explicit or schema type is provided
|
|
34
32
|
* @param options - Collection options with optional utilities
|
|
35
33
|
* @returns A new Collection with utilities exposed both at top level and under .utils
|
|
36
34
|
*
|
|
@@ -94,26 +92,22 @@ export interface Collection<T extends object = Record<string, unknown>, TKey ext
|
|
|
94
92
|
* sync: { sync: () => {} }
|
|
95
93
|
* })
|
|
96
94
|
*
|
|
97
|
-
* // Note: You can provide an explicit type, a schema, or both. When both are provided, the explicit type takes precedence.
|
|
98
95
|
*/
|
|
99
|
-
export declare function createCollection<
|
|
100
|
-
schema:
|
|
96
|
+
export declare function createCollection<T extends StandardSchemaV1, TKey extends string | number = string | number, TUtils extends UtilsRecord = {}>(options: CollectionConfig<InferSchemaOutput<T>, TKey, T> & {
|
|
97
|
+
schema: T;
|
|
101
98
|
utils?: TUtils;
|
|
102
|
-
}): Collection<
|
|
103
|
-
export declare function createCollection<
|
|
104
|
-
schema
|
|
99
|
+
}): Collection<InferSchemaOutput<T>, TKey, TUtils, T, InferSchemaInput<T>>;
|
|
100
|
+
export declare function createCollection<T extends object, TKey extends string | number = string | number, TUtils extends UtilsRecord = {}>(options: CollectionConfig<T, TKey, never> & {
|
|
101
|
+
schema?: never;
|
|
105
102
|
utils?: TUtils;
|
|
106
|
-
}): Collection<
|
|
107
|
-
export declare
|
|
108
|
-
|
|
109
|
-
}): Collection<ResolveType<TExplicit, TSchema, TFallback>, TKey, TUtils, TSchema, ResolveInsertInput<TExplicit, TSchema, TFallback>>;
|
|
110
|
-
export declare class CollectionImpl<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = {}, TSchema extends StandardSchemaV1 = StandardSchemaV1, TInsertInput extends object = T> {
|
|
111
|
-
config: CollectionConfig<T, TKey, TSchema, TInsertInput>;
|
|
103
|
+
}): Collection<T, TKey, TUtils, never, T>;
|
|
104
|
+
export declare class CollectionImpl<TOutput extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = {}, TSchema extends StandardSchemaV1 = StandardSchemaV1, TInput extends object = TOutput> {
|
|
105
|
+
config: CollectionConfig<TOutput, TKey, TSchema>;
|
|
112
106
|
transactions: SortedMap<string, Transaction<any>>;
|
|
113
|
-
pendingSyncedTransactions: Array<PendingSyncedTransaction<
|
|
114
|
-
syncedData: Map<TKey,
|
|
107
|
+
pendingSyncedTransactions: Array<PendingSyncedTransaction<TOutput>>;
|
|
108
|
+
syncedData: Map<TKey, TOutput> | SortedMap<TKey, TOutput>;
|
|
115
109
|
syncedMetadata: Map<TKey, unknown>;
|
|
116
|
-
optimisticUpserts: Map<TKey,
|
|
110
|
+
optimisticUpserts: Map<TKey, TOutput>;
|
|
117
111
|
optimisticDeletes: Set<TKey>;
|
|
118
112
|
private _size;
|
|
119
113
|
private lazyIndexes;
|
|
@@ -194,7 +188,7 @@ export declare class CollectionImpl<T extends object = Record<string, unknown>,
|
|
|
194
188
|
* @param config - Configuration object for the collection
|
|
195
189
|
* @throws Error if sync config is missing
|
|
196
190
|
*/
|
|
197
|
-
constructor(config: CollectionConfig<
|
|
191
|
+
constructor(config: CollectionConfig<TOutput, TKey, TSchema>);
|
|
198
192
|
/**
|
|
199
193
|
* Start sync immediately - internal method for compiled queries
|
|
200
194
|
* This bypasses lazy loading for special cases like live query results
|
|
@@ -261,7 +255,7 @@ export declare class CollectionImpl<T extends object = Record<string, unknown>,
|
|
|
261
255
|
/**
|
|
262
256
|
* Get the current value for a key (virtual derived state)
|
|
263
257
|
*/
|
|
264
|
-
get(key: TKey):
|
|
258
|
+
get(key: TKey): TOutput | undefined;
|
|
265
259
|
/**
|
|
266
260
|
* Check if a key exists in the collection (virtual derived state)
|
|
267
261
|
*/
|
|
@@ -277,23 +271,23 @@ export declare class CollectionImpl<T extends object = Record<string, unknown>,
|
|
|
277
271
|
/**
|
|
278
272
|
* Get all values (virtual derived state)
|
|
279
273
|
*/
|
|
280
|
-
values(): IterableIterator<
|
|
274
|
+
values(): IterableIterator<TOutput>;
|
|
281
275
|
/**
|
|
282
276
|
* Get all entries (virtual derived state)
|
|
283
277
|
*/
|
|
284
|
-
entries(): IterableIterator<[TKey,
|
|
278
|
+
entries(): IterableIterator<[TKey, TOutput]>;
|
|
285
279
|
/**
|
|
286
280
|
* Get all entries (virtual derived state)
|
|
287
281
|
*/
|
|
288
|
-
[Symbol.iterator](): IterableIterator<[TKey,
|
|
282
|
+
[Symbol.iterator](): IterableIterator<[TKey, TOutput]>;
|
|
289
283
|
/**
|
|
290
284
|
* Execute a callback for each entry in the collection
|
|
291
285
|
*/
|
|
292
|
-
forEach(callbackfn: (value:
|
|
286
|
+
forEach(callbackfn: (value: TOutput, key: TKey, index: number) => void): void;
|
|
293
287
|
/**
|
|
294
288
|
* Create a new array with the results of calling a function for each entry in the collection
|
|
295
289
|
*/
|
|
296
|
-
map<U>(callbackfn: (value:
|
|
290
|
+
map<U>(callbackfn: (value: TOutput, key: TKey, index: number) => U): Array<U>;
|
|
297
291
|
/**
|
|
298
292
|
* Attempts to commit pending synced transactions if there are no active transactions
|
|
299
293
|
* This method processes operations from pending transactions and applies them to the synced data
|
|
@@ -305,7 +299,7 @@ export declare class CollectionImpl<T extends object = Record<string, unknown>,
|
|
|
305
299
|
*/
|
|
306
300
|
private scheduleTransactionCleanup;
|
|
307
301
|
private ensureStandardSchema;
|
|
308
|
-
getKeyFromItem(item:
|
|
302
|
+
getKeyFromItem(item: TOutput): TKey;
|
|
309
303
|
generateGlobalKey(key: any, item: any): string;
|
|
310
304
|
/**
|
|
311
305
|
* Creates an index on a collection for faster queries.
|
|
@@ -337,7 +331,7 @@ export declare class CollectionImpl<T extends object = Record<string, unknown>,
|
|
|
337
331
|
* options: { language: 'en' }
|
|
338
332
|
* })
|
|
339
333
|
*/
|
|
340
|
-
createIndex<TResolver extends IndexResolver<TKey> = typeof BTreeIndex>(indexCallback: (row: SingleRowRefProxy<
|
|
334
|
+
createIndex<TResolver extends IndexResolver<TKey> = typeof BTreeIndex>(indexCallback: (row: SingleRowRefProxy<TOutput>) => any, config?: IndexOptions<TResolver>): IndexProxy<TKey>;
|
|
341
335
|
/**
|
|
342
336
|
* Resolve all lazy indexes (called when collection first syncs)
|
|
343
337
|
* @private
|
|
@@ -357,7 +351,7 @@ export declare class CollectionImpl<T extends object = Record<string, unknown>,
|
|
|
357
351
|
* @private
|
|
358
352
|
*/
|
|
359
353
|
private updateIndexes;
|
|
360
|
-
validateData(data: unknown, type: `insert` | `update`, key?: TKey):
|
|
354
|
+
validateData(data: unknown, type: `insert` | `update`, key?: TKey): TOutput | never;
|
|
361
355
|
/**
|
|
362
356
|
* Inserts one or more items into the collection
|
|
363
357
|
* @param items - Single item or array of items to insert
|
|
@@ -394,7 +388,7 @@ export declare class CollectionImpl<T extends object = Record<string, unknown>,
|
|
|
394
388
|
* console.log('Insert failed:', error)
|
|
395
389
|
* }
|
|
396
390
|
*/
|
|
397
|
-
insert: (data:
|
|
391
|
+
insert: (data: TInput | Array<TInput>, config?: InsertConfig) => Transaction<Record<string, unknown>> | Transaction<TOutput>;
|
|
398
392
|
/**
|
|
399
393
|
* Updates one or more items in the collection using a callback function
|
|
400
394
|
* @param keys - Single key or array of keys to update
|
|
@@ -434,10 +428,10 @@ export declare class CollectionImpl<T extends object = Record<string, unknown>,
|
|
|
434
428
|
* console.log('Update failed:', error)
|
|
435
429
|
* }
|
|
436
430
|
*/
|
|
437
|
-
update
|
|
438
|
-
update
|
|
439
|
-
update
|
|
440
|
-
update
|
|
431
|
+
update(key: Array<TKey | unknown>, callback: (drafts: Array<WritableDeep<TInput>>) => void): TransactionType;
|
|
432
|
+
update(keys: Array<TKey | unknown>, config: OperationConfig, callback: (drafts: Array<WritableDeep<TInput>>) => void): TransactionType;
|
|
433
|
+
update(id: TKey | unknown, callback: (draft: WritableDeep<TInput>) => void): TransactionType;
|
|
434
|
+
update(id: TKey | unknown, config: OperationConfig, callback: (draft: WritableDeep<TInput>) => void): TransactionType;
|
|
441
435
|
/**
|
|
442
436
|
* Deletes one or more items from the collection
|
|
443
437
|
* @param keys - Single key or array of keys to delete
|
|
@@ -485,27 +479,27 @@ export declare class CollectionImpl<T extends object = Record<string, unknown>,
|
|
|
485
479
|
* console.log("Todo 1 exists:", itemsMap.get("todo-1"))
|
|
486
480
|
* }
|
|
487
481
|
*/
|
|
488
|
-
get state(): Map<TKey,
|
|
482
|
+
get state(): Map<TKey, TOutput>;
|
|
489
483
|
/**
|
|
490
484
|
* Gets the current state of the collection as a Map, but only resolves when data is available
|
|
491
485
|
* Waits for the first sync commit to complete before resolving
|
|
492
486
|
*
|
|
493
487
|
* @returns Promise that resolves to a Map containing all items in the collection
|
|
494
488
|
*/
|
|
495
|
-
stateWhenReady(): Promise<Map<TKey,
|
|
489
|
+
stateWhenReady(): Promise<Map<TKey, TOutput>>;
|
|
496
490
|
/**
|
|
497
491
|
* Gets the current state of the collection as an Array
|
|
498
492
|
*
|
|
499
493
|
* @returns An Array containing all items in the collection
|
|
500
494
|
*/
|
|
501
|
-
get toArray():
|
|
495
|
+
get toArray(): TOutput[];
|
|
502
496
|
/**
|
|
503
497
|
* Gets the current state of the collection as an Array, but only resolves when data is available
|
|
504
498
|
* Waits for the first sync commit to complete before resolving
|
|
505
499
|
*
|
|
506
500
|
* @returns Promise that resolves to an Array containing all items in the collection
|
|
507
501
|
*/
|
|
508
|
-
toArrayWhenReady(): Promise<Array<
|
|
502
|
+
toArrayWhenReady(): Promise<Array<TOutput>>;
|
|
509
503
|
/**
|
|
510
504
|
* Returns the current state of the collection as an array of changes
|
|
511
505
|
* @param options - Options including optional where filter
|
|
@@ -524,7 +518,7 @@ export declare class CollectionImpl<T extends object = Record<string, unknown>,
|
|
|
524
518
|
* whereExpression: eq(row.status, 'active')
|
|
525
519
|
* })
|
|
526
520
|
*/
|
|
527
|
-
currentStateAsChanges(options?: CurrentStateAsChangesOptions<
|
|
521
|
+
currentStateAsChanges(options?: CurrentStateAsChangesOptions<TOutput>): Array<ChangeMessage<TOutput>>;
|
|
528
522
|
/**
|
|
529
523
|
* Subscribe to changes in the collection
|
|
530
524
|
* @param callback - Function called when items change
|
|
@@ -564,11 +558,11 @@ export declare class CollectionImpl<T extends object = Record<string, unknown>,
|
|
|
564
558
|
* whereExpression: eq(row.status, 'active')
|
|
565
559
|
* })
|
|
566
560
|
*/
|
|
567
|
-
subscribeChanges(callback: (changes: Array<ChangeMessage<
|
|
561
|
+
subscribeChanges(callback: (changes: Array<ChangeMessage<TOutput>>) => void, options?: SubscribeChangesOptions<TOutput>): () => void;
|
|
568
562
|
/**
|
|
569
563
|
* Subscribe to changes for a specific key
|
|
570
564
|
*/
|
|
571
|
-
subscribeChangesKey(key: TKey, listener: ChangeListener<
|
|
565
|
+
subscribeChangesKey(key: TKey, listener: ChangeListener<TOutput, TKey>, { includeInitialState }?: {
|
|
572
566
|
includeInitialState?: boolean;
|
|
573
567
|
}): () => void;
|
|
574
568
|
/**
|