@tanstack/db 0.4.6 → 0.4.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/mutations.cjs +4 -4
- package/dist/cjs/collection/mutations.cjs.map +1 -1
- package/dist/cjs/local-only.cjs +21 -2
- package/dist/cjs/local-only.cjs.map +1 -1
- package/dist/cjs/local-only.d.cts +64 -7
- package/dist/cjs/local-storage.cjs +71 -3
- package/dist/cjs/local-storage.cjs.map +1 -1
- package/dist/cjs/local-storage.d.cts +55 -2
- package/dist/esm/collection/mutations.js +4 -4
- package/dist/esm/collection/mutations.js.map +1 -1
- package/dist/esm/local-only.d.ts +64 -7
- package/dist/esm/local-only.js +21 -2
- package/dist/esm/local-only.js.map +1 -1
- package/dist/esm/local-storage.d.ts +55 -2
- package/dist/esm/local-storage.js +72 -4
- package/dist/esm/local-storage.js.map +1 -1
- package/package.json +1 -1
- package/src/collection/mutations.ts +8 -4
- package/src/local-only.ts +119 -30
- package/src/local-storage.ts +170 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseCollectionConfig, CollectionConfig, InferSchemaOutput, UtilsRecord } from './types.cjs';
|
|
1
|
+
import { BaseCollectionConfig, CollectionConfig, InferSchemaOutput, PendingMutation, UtilsRecord } from './types.cjs';
|
|
2
2
|
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
3
3
|
/**
|
|
4
4
|
* Storage API interface - subset of DOM Storage that we need
|
|
@@ -47,6 +47,26 @@ export type GetStorageSizeFn = () => number;
|
|
|
47
47
|
export interface LocalStorageCollectionUtils extends UtilsRecord {
|
|
48
48
|
clearStorage: ClearStorageFn;
|
|
49
49
|
getStorageSize: GetStorageSizeFn;
|
|
50
|
+
/**
|
|
51
|
+
* Accepts mutations from a transaction that belong to this collection and persists them to localStorage.
|
|
52
|
+
* This should be called in your transaction's mutationFn to persist local-storage data.
|
|
53
|
+
*
|
|
54
|
+
* @param transaction - The transaction containing mutations to accept
|
|
55
|
+
* @example
|
|
56
|
+
* const localSettings = createCollection(localStorageCollectionOptions({...}))
|
|
57
|
+
*
|
|
58
|
+
* const tx = createTransaction({
|
|
59
|
+
* mutationFn: async ({ transaction }) => {
|
|
60
|
+
* // Make API call first
|
|
61
|
+
* await api.save(...)
|
|
62
|
+
* // Then persist local-storage mutations after success
|
|
63
|
+
* localSettings.utils.acceptMutations(transaction)
|
|
64
|
+
* }
|
|
65
|
+
* })
|
|
66
|
+
*/
|
|
67
|
+
acceptMutations: (transaction: {
|
|
68
|
+
mutations: Array<PendingMutation<Record<string, unknown>>>;
|
|
69
|
+
}) => void;
|
|
50
70
|
}
|
|
51
71
|
/**
|
|
52
72
|
* Creates localStorage collection options for use with a standard Collection
|
|
@@ -54,11 +74,17 @@ export interface LocalStorageCollectionUtils extends UtilsRecord {
|
|
|
54
74
|
* This function creates a collection that persists data to localStorage/sessionStorage
|
|
55
75
|
* and synchronizes changes across browser tabs using storage events.
|
|
56
76
|
*
|
|
77
|
+
* **Using with Manual Transactions:**
|
|
78
|
+
*
|
|
79
|
+
* For manual transactions, you must call `utils.acceptMutations()` in your transaction's `mutationFn`
|
|
80
|
+
* to persist changes made during `tx.mutate()`. This is necessary because local-storage collections
|
|
81
|
+
* don't participate in the standard mutation handler flow for manual transactions.
|
|
82
|
+
*
|
|
57
83
|
* @template TExplicit - The explicit type of items in the collection (highest priority)
|
|
58
84
|
* @template TSchema - The schema type for validation and type inference (second priority)
|
|
59
85
|
* @template TFallback - The fallback type if no explicit or schema type is provided
|
|
60
86
|
* @param config - Configuration options for the localStorage collection
|
|
61
|
-
* @returns Collection options with utilities including clearStorage and
|
|
87
|
+
* @returns Collection options with utilities including clearStorage, getStorageSize, and acceptMutations
|
|
62
88
|
*
|
|
63
89
|
* @example
|
|
64
90
|
* // Basic localStorage collection
|
|
@@ -90,6 +116,33 @@ export interface LocalStorageCollectionUtils extends UtilsRecord {
|
|
|
90
116
|
* },
|
|
91
117
|
* })
|
|
92
118
|
* )
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* // Using with manual transactions
|
|
122
|
+
* const localSettings = createCollection(
|
|
123
|
+
* localStorageCollectionOptions({
|
|
124
|
+
* storageKey: 'user-settings',
|
|
125
|
+
* getKey: (item) => item.id,
|
|
126
|
+
* })
|
|
127
|
+
* )
|
|
128
|
+
*
|
|
129
|
+
* const tx = createTransaction({
|
|
130
|
+
* mutationFn: async ({ transaction }) => {
|
|
131
|
+
* // Use settings data in API call
|
|
132
|
+
* const settingsMutations = transaction.mutations.filter(m => m.collection === localSettings)
|
|
133
|
+
* await api.updateUserProfile({ settings: settingsMutations[0]?.modified })
|
|
134
|
+
*
|
|
135
|
+
* // Persist local-storage mutations after API success
|
|
136
|
+
* localSettings.utils.acceptMutations(transaction)
|
|
137
|
+
* }
|
|
138
|
+
* })
|
|
139
|
+
*
|
|
140
|
+
* tx.mutate(() => {
|
|
141
|
+
* localSettings.insert({ id: 'theme', value: 'dark' })
|
|
142
|
+
* apiCollection.insert({ id: 2, data: 'profile data' })
|
|
143
|
+
* })
|
|
144
|
+
*
|
|
145
|
+
* await tx.commit()
|
|
93
146
|
*/
|
|
94
147
|
export declare function localStorageCollectionOptions<T extends StandardSchemaV1, TKey extends string | number = string | number>(config: LocalStorageCollectionConfig<InferSchemaOutput<T>, T, TKey> & {
|
|
95
148
|
schema: T;
|
|
@@ -60,7 +60,7 @@ class CollectionMutationsManager {
|
|
|
60
60
|
}
|
|
61
61
|
});
|
|
62
62
|
directOpTransaction.applyMutations(mutations);
|
|
63
|
-
directOpTransaction.commit();
|
|
63
|
+
directOpTransaction.commit().catch(() => void 0);
|
|
64
64
|
state.transactions.set(directOpTransaction.id, directOpTransaction);
|
|
65
65
|
state.scheduleTransactionCleanup(directOpTransaction);
|
|
66
66
|
state.recomputeOptimisticState(true);
|
|
@@ -118,7 +118,7 @@ class CollectionMutationsManager {
|
|
|
118
118
|
}
|
|
119
119
|
});
|
|
120
120
|
directOpTransaction.applyMutations(mutations);
|
|
121
|
-
directOpTransaction.commit();
|
|
121
|
+
directOpTransaction.commit().catch(() => void 0);
|
|
122
122
|
state.transactions.set(directOpTransaction.id, directOpTransaction);
|
|
123
123
|
state.scheduleTransactionCleanup(directOpTransaction);
|
|
124
124
|
state.recomputeOptimisticState(true);
|
|
@@ -275,7 +275,7 @@ class CollectionMutationsManager {
|
|
|
275
275
|
mutationFn: async () => {
|
|
276
276
|
}
|
|
277
277
|
});
|
|
278
|
-
emptyTransaction.commit();
|
|
278
|
+
emptyTransaction.commit().catch(() => void 0);
|
|
279
279
|
state.scheduleTransactionCleanup(emptyTransaction);
|
|
280
280
|
return emptyTransaction;
|
|
281
281
|
}
|
|
@@ -295,7 +295,7 @@ class CollectionMutationsManager {
|
|
|
295
295
|
}
|
|
296
296
|
});
|
|
297
297
|
directOpTransaction.applyMutations(mutations);
|
|
298
|
-
directOpTransaction.commit();
|
|
298
|
+
directOpTransaction.commit().catch(() => void 0);
|
|
299
299
|
state.transactions.set(directOpTransaction.id, directOpTransaction);
|
|
300
300
|
state.scheduleTransactionCleanup(directOpTransaction);
|
|
301
301
|
state.recomputeOptimisticState(true);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mutations.js","sources":["../../../src/collection/mutations.ts"],"sourcesContent":["import { withArrayChangeTracking, withChangeTracking } from \"../proxy\"\nimport { createTransaction, getActiveTransaction } from \"../transactions\"\nimport {\n DeleteKeyNotFoundError,\n DuplicateKeyError,\n InvalidSchemaError,\n KeyUpdateNotAllowedError,\n MissingDeleteHandlerError,\n MissingInsertHandlerError,\n MissingUpdateArgumentError,\n MissingUpdateHandlerError,\n NoKeysPassedToDeleteError,\n NoKeysPassedToUpdateError,\n SchemaMustBeSynchronousError,\n SchemaValidationError,\n UndefinedKeyError,\n UpdateKeyNotFoundError,\n} from \"../errors\"\nimport type { Collection, CollectionImpl } from \"./index.js\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type {\n CollectionConfig,\n InsertConfig,\n OperationConfig,\n PendingMutation,\n StandardSchema,\n Transaction as TransactionType,\n TransactionWithMutations,\n UtilsRecord,\n WritableDeep,\n} from \"../types\"\nimport type { CollectionLifecycleManager } from \"./lifecycle\"\nimport type { CollectionStateManager } from \"./state\"\n\nexport class CollectionMutationsManager<\n TOutput extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = {},\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInput extends object = TOutput,\n> {\n private lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n private collection!: CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>\n private config!: CollectionConfig<TOutput, TKey, TSchema>\n private id: string\n\n constructor(config: CollectionConfig<TOutput, TKey, TSchema>, id: string) {\n this.id = id\n this.config = config\n }\n\n setDeps(deps: {\n lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n state: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n collection: CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>\n }) {\n this.lifecycle = deps.lifecycle\n this.state = deps.state\n this.collection = deps.collection\n }\n\n private ensureStandardSchema(schema: unknown): StandardSchema<TOutput> {\n // If the schema already implements the standard-schema interface, return it\n if (schema && `~standard` in (schema as {})) {\n return schema as StandardSchema<TOutput>\n }\n\n throw new InvalidSchemaError()\n }\n\n public validateData(\n data: unknown,\n type: `insert` | `update`,\n key?: TKey\n ): TOutput | never {\n if (!this.config.schema) return data as TOutput\n\n const standardSchema = this.ensureStandardSchema(this.config.schema)\n\n // For updates, we need to merge with the existing data before validation\n if (type === `update` && key) {\n // Get the existing data for this key\n const existingData = this.state.get(key)\n\n if (\n existingData &&\n data &&\n typeof data === `object` &&\n typeof existingData === `object`\n ) {\n // Merge the update with the existing data\n const mergedData = Object.assign({}, existingData, data)\n\n // Validate the merged data\n const result = standardSchema[`~standard`].validate(mergedData)\n\n // Ensure validation is synchronous\n if (result instanceof Promise) {\n throw new SchemaMustBeSynchronousError()\n }\n\n // If validation fails, throw a SchemaValidationError with the issues\n if (`issues` in result && result.issues) {\n const typedIssues = result.issues.map((issue) => ({\n message: issue.message,\n path: issue.path?.map((p) => String(p)),\n }))\n throw new SchemaValidationError(type, typedIssues)\n }\n\n // Extract only the modified keys from the validated result\n const validatedMergedData = result.value as TOutput\n const modifiedKeys = Object.keys(data)\n const extractedChanges = Object.fromEntries(\n modifiedKeys.map((k) => [k, validatedMergedData[k as keyof TOutput]])\n ) as TOutput\n\n return extractedChanges\n }\n }\n\n // For inserts or updates without existing data, validate the data directly\n const result = standardSchema[`~standard`].validate(data)\n\n // Ensure validation is synchronous\n if (result instanceof Promise) {\n throw new SchemaMustBeSynchronousError()\n }\n\n // If validation fails, throw a SchemaValidationError with the issues\n if (`issues` in result && result.issues) {\n const typedIssues = result.issues.map((issue) => ({\n message: issue.message,\n path: issue.path?.map((p) => String(p)),\n }))\n throw new SchemaValidationError(type, typedIssues)\n }\n\n return result.value as TOutput\n }\n\n public generateGlobalKey(key: any, item: any): string {\n if (typeof key === `undefined`) {\n throw new UndefinedKeyError(item)\n }\n\n return `KEY::${this.id}/${key}`\n }\n\n /**\n * Inserts one or more items into the collection\n */\n insert = (data: TInput | Array<TInput>, config?: InsertConfig) => {\n this.lifecycle.validateCollectionUsable(`insert`)\n const state = this.state\n const ambientTransaction = getActiveTransaction()\n\n // If no ambient transaction exists, check for an onInsert handler early\n if (!ambientTransaction && !this.config.onInsert) {\n throw new MissingInsertHandlerError()\n }\n\n const items = Array.isArray(data) ? data : [data]\n const mutations: Array<PendingMutation<TOutput>> = []\n\n // Create mutations for each item\n items.forEach((item) => {\n // Validate the data against the schema if one exists\n const validatedData = this.validateData(item, `insert`)\n\n // Check if an item with this ID already exists in the collection\n const key = this.config.getKey(validatedData)\n if (this.state.has(key)) {\n throw new DuplicateKeyError(key)\n }\n const globalKey = this.generateGlobalKey(key, item)\n\n const mutation: PendingMutation<TOutput, `insert`> = {\n mutationId: crypto.randomUUID(),\n original: {},\n modified: validatedData,\n // Pick the values from validatedData based on what's passed in - this is for cases\n // where a schema has default values. The validated data has the extra default\n // values but for changes, we just want to show the data that was actually passed in.\n changes: Object.fromEntries(\n Object.keys(item).map((k) => [\n k,\n validatedData[k as keyof typeof validatedData],\n ])\n ) as TInput,\n globalKey,\n key,\n metadata: config?.metadata as unknown,\n syncMetadata: this.config.sync.getSyncMetadata?.() || {},\n optimistic: config?.optimistic ?? true,\n type: `insert`,\n createdAt: new Date(),\n updatedAt: new Date(),\n collection: this.collection,\n }\n\n mutations.push(mutation)\n })\n\n // If an ambient transaction exists, use it\n if (ambientTransaction) {\n ambientTransaction.applyMutations(mutations)\n\n state.transactions.set(ambientTransaction.id, ambientTransaction)\n state.scheduleTransactionCleanup(ambientTransaction)\n state.recomputeOptimisticState(true)\n\n return ambientTransaction\n } else {\n // Create a new transaction with a mutation function that calls the onInsert handler\n const directOpTransaction = createTransaction<TOutput>({\n mutationFn: async (params) => {\n // Call the onInsert handler with the transaction and collection\n return await this.config.onInsert!({\n transaction:\n params.transaction as unknown as TransactionWithMutations<\n TOutput,\n `insert`\n >,\n collection: this.collection as unknown as Collection<TOutput, TKey>,\n })\n },\n })\n\n // Apply mutations to the new transaction\n directOpTransaction.applyMutations(mutations)\n directOpTransaction.commit()\n\n // Add the transaction to the collection's transactions store\n state.transactions.set(directOpTransaction.id, directOpTransaction)\n state.scheduleTransactionCleanup(directOpTransaction)\n state.recomputeOptimisticState(true)\n\n return directOpTransaction\n }\n }\n\n /**\n * Updates one or more items in the collection using a callback function\n */\n update(\n keys: (TKey | unknown) | Array<TKey | unknown>,\n configOrCallback:\n | ((draft: WritableDeep<TInput>) => void)\n | ((drafts: Array<WritableDeep<TInput>>) => void)\n | OperationConfig,\n maybeCallback?:\n | ((draft: WritableDeep<TInput>) => void)\n | ((drafts: Array<WritableDeep<TInput>>) => void)\n ) {\n if (typeof keys === `undefined`) {\n throw new MissingUpdateArgumentError()\n }\n\n const state = this.state\n this.lifecycle.validateCollectionUsable(`update`)\n\n const ambientTransaction = getActiveTransaction()\n\n // If no ambient transaction exists, check for an onUpdate handler early\n if (!ambientTransaction && !this.config.onUpdate) {\n throw new MissingUpdateHandlerError()\n }\n\n const isArray = Array.isArray(keys)\n const keysArray = isArray ? keys : [keys]\n\n if (isArray && keysArray.length === 0) {\n throw new NoKeysPassedToUpdateError()\n }\n\n const callback =\n typeof configOrCallback === `function` ? configOrCallback : maybeCallback!\n const config =\n typeof configOrCallback === `function` ? {} : configOrCallback\n\n // Get the current objects or empty objects if they don't exist\n const currentObjects = keysArray.map((key) => {\n const item = this.state.get(key)\n if (!item) {\n throw new UpdateKeyNotFoundError(key)\n }\n\n return item\n }) as unknown as Array<TInput>\n\n let changesArray\n if (isArray) {\n // Use the proxy to track changes for all objects\n changesArray = withArrayChangeTracking(\n currentObjects,\n callback as (draft: Array<TInput>) => void\n )\n } else {\n const result = withChangeTracking(\n currentObjects[0]!,\n callback as (draft: TInput) => void\n )\n changesArray = [result]\n }\n\n // Create mutations for each object that has changes\n const mutations: Array<\n PendingMutation<\n TOutput,\n `update`,\n CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>\n >\n > = keysArray\n .map((key, index) => {\n const itemChanges = changesArray[index] // User-provided changes for this specific item\n\n // Skip items with no changes\n if (!itemChanges || Object.keys(itemChanges).length === 0) {\n return null\n }\n\n const originalItem = currentObjects[index] as unknown as TOutput\n // Validate the user-provided changes for this item\n const validatedUpdatePayload = this.validateData(\n itemChanges,\n `update`,\n key\n )\n\n // Construct the full modified item by applying the validated update payload to the original item\n const modifiedItem = Object.assign(\n {},\n originalItem,\n validatedUpdatePayload\n )\n\n // Check if the ID of the item is being changed\n const originalItemId = this.config.getKey(originalItem)\n const modifiedItemId = this.config.getKey(modifiedItem)\n\n if (originalItemId !== modifiedItemId) {\n throw new KeyUpdateNotAllowedError(originalItemId, modifiedItemId)\n }\n\n const globalKey = this.generateGlobalKey(modifiedItemId, modifiedItem)\n\n return {\n mutationId: crypto.randomUUID(),\n original: originalItem,\n modified: modifiedItem,\n // Pick the values from modifiedItem based on what's passed in - this is for cases\n // where a schema has default values or transforms. The modified data has the extra\n // default or transformed values but for changes, we just want to show the data that\n // was actually passed in.\n changes: Object.fromEntries(\n Object.keys(itemChanges).map((k) => [\n k,\n modifiedItem[k as keyof typeof modifiedItem],\n ])\n ) as TInput,\n globalKey,\n key,\n metadata: config.metadata as unknown,\n syncMetadata: (state.syncedMetadata.get(key) || {}) as Record<\n string,\n unknown\n >,\n optimistic: config.optimistic ?? true,\n type: `update`,\n createdAt: new Date(),\n updatedAt: new Date(),\n collection: this.collection,\n }\n })\n .filter(Boolean) as Array<\n PendingMutation<\n TOutput,\n `update`,\n CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>\n >\n >\n\n // If no changes were made, return an empty transaction early\n if (mutations.length === 0) {\n const emptyTransaction = createTransaction({\n mutationFn: async () => {},\n })\n emptyTransaction.commit()\n // Schedule cleanup for empty transaction\n state.scheduleTransactionCleanup(emptyTransaction)\n return emptyTransaction\n }\n\n // If an ambient transaction exists, use it\n if (ambientTransaction) {\n ambientTransaction.applyMutations(mutations)\n\n state.transactions.set(ambientTransaction.id, ambientTransaction)\n state.scheduleTransactionCleanup(ambientTransaction)\n state.recomputeOptimisticState(true)\n\n return ambientTransaction\n }\n\n // No need to check for onUpdate handler here as we've already checked at the beginning\n\n // Create a new transaction with a mutation function that calls the onUpdate handler\n const directOpTransaction = createTransaction<TOutput>({\n mutationFn: async (params) => {\n // Call the onUpdate handler with the transaction and collection\n return this.config.onUpdate!({\n transaction:\n params.transaction as unknown as TransactionWithMutations<\n TOutput,\n `update`\n >,\n collection: this.collection as unknown as Collection<TOutput, TKey>,\n })\n },\n })\n\n // Apply mutations to the new transaction\n directOpTransaction.applyMutations(mutations)\n directOpTransaction.commit()\n\n // Add the transaction to the collection's transactions store\n\n state.transactions.set(directOpTransaction.id, directOpTransaction)\n state.scheduleTransactionCleanup(directOpTransaction)\n state.recomputeOptimisticState(true)\n\n return directOpTransaction\n }\n\n /**\n * Deletes one or more items from the collection\n */\n delete = (\n keys: Array<TKey> | TKey,\n config?: OperationConfig\n ): TransactionType<any> => {\n const state = this.state\n this.lifecycle.validateCollectionUsable(`delete`)\n\n const ambientTransaction = getActiveTransaction()\n\n // If no ambient transaction exists, check for an onDelete handler early\n if (!ambientTransaction && !this.config.onDelete) {\n throw new MissingDeleteHandlerError()\n }\n\n if (Array.isArray(keys) && keys.length === 0) {\n throw new NoKeysPassedToDeleteError()\n }\n\n const keysArray = Array.isArray(keys) ? keys : [keys]\n const mutations: Array<\n PendingMutation<\n TOutput,\n `delete`,\n CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>\n >\n > = []\n\n for (const key of keysArray) {\n if (!this.state.has(key)) {\n throw new DeleteKeyNotFoundError(key)\n }\n const globalKey = this.generateGlobalKey(key, this.state.get(key)!)\n const mutation: PendingMutation<\n TOutput,\n `delete`,\n CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>\n > = {\n mutationId: crypto.randomUUID(),\n original: this.state.get(key)!,\n modified: this.state.get(key)!,\n changes: this.state.get(key)!,\n globalKey,\n key,\n metadata: config?.metadata as unknown,\n syncMetadata: (state.syncedMetadata.get(key) || {}) as Record<\n string,\n unknown\n >,\n optimistic: config?.optimistic ?? true,\n type: `delete`,\n createdAt: new Date(),\n updatedAt: new Date(),\n collection: this.collection,\n }\n\n mutations.push(mutation)\n }\n\n // If an ambient transaction exists, use it\n if (ambientTransaction) {\n ambientTransaction.applyMutations(mutations)\n\n state.transactions.set(ambientTransaction.id, ambientTransaction)\n state.scheduleTransactionCleanup(ambientTransaction)\n state.recomputeOptimisticState(true)\n\n return ambientTransaction\n }\n\n // Create a new transaction with a mutation function that calls the onDelete handler\n const directOpTransaction = createTransaction<TOutput>({\n autoCommit: true,\n mutationFn: async (params) => {\n // Call the onDelete handler with the transaction and collection\n return this.config.onDelete!({\n transaction:\n params.transaction as unknown as TransactionWithMutations<\n TOutput,\n `delete`\n >,\n collection: this.collection as unknown as Collection<TOutput, TKey>,\n })\n },\n })\n\n // Apply mutations to the new transaction\n directOpTransaction.applyMutations(mutations)\n directOpTransaction.commit()\n\n state.transactions.set(directOpTransaction.id, directOpTransaction)\n state.scheduleTransactionCleanup(directOpTransaction)\n state.recomputeOptimisticState(true)\n\n return directOpTransaction\n }\n}\n"],"names":["config","result"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mutations.js","sources":["../../../src/collection/mutations.ts"],"sourcesContent":["import { withArrayChangeTracking, withChangeTracking } from \"../proxy\"\nimport { createTransaction, getActiveTransaction } from \"../transactions\"\nimport {\n DeleteKeyNotFoundError,\n DuplicateKeyError,\n InvalidSchemaError,\n KeyUpdateNotAllowedError,\n MissingDeleteHandlerError,\n MissingInsertHandlerError,\n MissingUpdateArgumentError,\n MissingUpdateHandlerError,\n NoKeysPassedToDeleteError,\n NoKeysPassedToUpdateError,\n SchemaMustBeSynchronousError,\n SchemaValidationError,\n UndefinedKeyError,\n UpdateKeyNotFoundError,\n} from \"../errors\"\nimport type { Collection, CollectionImpl } from \"./index.js\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type {\n CollectionConfig,\n InsertConfig,\n OperationConfig,\n PendingMutation,\n StandardSchema,\n Transaction as TransactionType,\n TransactionWithMutations,\n UtilsRecord,\n WritableDeep,\n} from \"../types\"\nimport type { CollectionLifecycleManager } from \"./lifecycle\"\nimport type { CollectionStateManager } from \"./state\"\n\nexport class CollectionMutationsManager<\n TOutput extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = {},\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInput extends object = TOutput,\n> {\n private lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n private collection!: CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>\n private config!: CollectionConfig<TOutput, TKey, TSchema>\n private id: string\n\n constructor(config: CollectionConfig<TOutput, TKey, TSchema>, id: string) {\n this.id = id\n this.config = config\n }\n\n setDeps(deps: {\n lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n state: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n collection: CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>\n }) {\n this.lifecycle = deps.lifecycle\n this.state = deps.state\n this.collection = deps.collection\n }\n\n private ensureStandardSchema(schema: unknown): StandardSchema<TOutput> {\n // If the schema already implements the standard-schema interface, return it\n if (schema && `~standard` in (schema as {})) {\n return schema as StandardSchema<TOutput>\n }\n\n throw new InvalidSchemaError()\n }\n\n public validateData(\n data: unknown,\n type: `insert` | `update`,\n key?: TKey\n ): TOutput | never {\n if (!this.config.schema) return data as TOutput\n\n const standardSchema = this.ensureStandardSchema(this.config.schema)\n\n // For updates, we need to merge with the existing data before validation\n if (type === `update` && key) {\n // Get the existing data for this key\n const existingData = this.state.get(key)\n\n if (\n existingData &&\n data &&\n typeof data === `object` &&\n typeof existingData === `object`\n ) {\n // Merge the update with the existing data\n const mergedData = Object.assign({}, existingData, data)\n\n // Validate the merged data\n const result = standardSchema[`~standard`].validate(mergedData)\n\n // Ensure validation is synchronous\n if (result instanceof Promise) {\n throw new SchemaMustBeSynchronousError()\n }\n\n // If validation fails, throw a SchemaValidationError with the issues\n if (`issues` in result && result.issues) {\n const typedIssues = result.issues.map((issue) => ({\n message: issue.message,\n path: issue.path?.map((p) => String(p)),\n }))\n throw new SchemaValidationError(type, typedIssues)\n }\n\n // Extract only the modified keys from the validated result\n const validatedMergedData = result.value as TOutput\n const modifiedKeys = Object.keys(data)\n const extractedChanges = Object.fromEntries(\n modifiedKeys.map((k) => [k, validatedMergedData[k as keyof TOutput]])\n ) as TOutput\n\n return extractedChanges\n }\n }\n\n // For inserts or updates without existing data, validate the data directly\n const result = standardSchema[`~standard`].validate(data)\n\n // Ensure validation is synchronous\n if (result instanceof Promise) {\n throw new SchemaMustBeSynchronousError()\n }\n\n // If validation fails, throw a SchemaValidationError with the issues\n if (`issues` in result && result.issues) {\n const typedIssues = result.issues.map((issue) => ({\n message: issue.message,\n path: issue.path?.map((p) => String(p)),\n }))\n throw new SchemaValidationError(type, typedIssues)\n }\n\n return result.value as TOutput\n }\n\n public generateGlobalKey(key: any, item: any): string {\n if (typeof key === `undefined`) {\n throw new UndefinedKeyError(item)\n }\n\n return `KEY::${this.id}/${key}`\n }\n\n /**\n * Inserts one or more items into the collection\n */\n insert = (data: TInput | Array<TInput>, config?: InsertConfig) => {\n this.lifecycle.validateCollectionUsable(`insert`)\n const state = this.state\n const ambientTransaction = getActiveTransaction()\n\n // If no ambient transaction exists, check for an onInsert handler early\n if (!ambientTransaction && !this.config.onInsert) {\n throw new MissingInsertHandlerError()\n }\n\n const items = Array.isArray(data) ? data : [data]\n const mutations: Array<PendingMutation<TOutput>> = []\n\n // Create mutations for each item\n items.forEach((item) => {\n // Validate the data against the schema if one exists\n const validatedData = this.validateData(item, `insert`)\n\n // Check if an item with this ID already exists in the collection\n const key = this.config.getKey(validatedData)\n if (this.state.has(key)) {\n throw new DuplicateKeyError(key)\n }\n const globalKey = this.generateGlobalKey(key, item)\n\n const mutation: PendingMutation<TOutput, `insert`> = {\n mutationId: crypto.randomUUID(),\n original: {},\n modified: validatedData,\n // Pick the values from validatedData based on what's passed in - this is for cases\n // where a schema has default values. The validated data has the extra default\n // values but for changes, we just want to show the data that was actually passed in.\n changes: Object.fromEntries(\n Object.keys(item).map((k) => [\n k,\n validatedData[k as keyof typeof validatedData],\n ])\n ) as TInput,\n globalKey,\n key,\n metadata: config?.metadata as unknown,\n syncMetadata: this.config.sync.getSyncMetadata?.() || {},\n optimistic: config?.optimistic ?? true,\n type: `insert`,\n createdAt: new Date(),\n updatedAt: new Date(),\n collection: this.collection,\n }\n\n mutations.push(mutation)\n })\n\n // If an ambient transaction exists, use it\n if (ambientTransaction) {\n ambientTransaction.applyMutations(mutations)\n\n state.transactions.set(ambientTransaction.id, ambientTransaction)\n state.scheduleTransactionCleanup(ambientTransaction)\n state.recomputeOptimisticState(true)\n\n return ambientTransaction\n } else {\n // Create a new transaction with a mutation function that calls the onInsert handler\n const directOpTransaction = createTransaction<TOutput>({\n mutationFn: async (params) => {\n // Call the onInsert handler with the transaction and collection\n return await this.config.onInsert!({\n transaction:\n params.transaction as unknown as TransactionWithMutations<\n TOutput,\n `insert`\n >,\n collection: this.collection as unknown as Collection<TOutput, TKey>,\n })\n },\n })\n\n // Apply mutations to the new transaction\n directOpTransaction.applyMutations(mutations)\n // Errors still reject tx.isPersisted.promise; this catch only prevents global unhandled rejections\n directOpTransaction.commit().catch(() => undefined)\n\n // Add the transaction to the collection's transactions store\n state.transactions.set(directOpTransaction.id, directOpTransaction)\n state.scheduleTransactionCleanup(directOpTransaction)\n state.recomputeOptimisticState(true)\n\n return directOpTransaction\n }\n }\n\n /**\n * Updates one or more items in the collection using a callback function\n */\n update(\n keys: (TKey | unknown) | Array<TKey | unknown>,\n configOrCallback:\n | ((draft: WritableDeep<TInput>) => void)\n | ((drafts: Array<WritableDeep<TInput>>) => void)\n | OperationConfig,\n maybeCallback?:\n | ((draft: WritableDeep<TInput>) => void)\n | ((drafts: Array<WritableDeep<TInput>>) => void)\n ) {\n if (typeof keys === `undefined`) {\n throw new MissingUpdateArgumentError()\n }\n\n const state = this.state\n this.lifecycle.validateCollectionUsable(`update`)\n\n const ambientTransaction = getActiveTransaction()\n\n // If no ambient transaction exists, check for an onUpdate handler early\n if (!ambientTransaction && !this.config.onUpdate) {\n throw new MissingUpdateHandlerError()\n }\n\n const isArray = Array.isArray(keys)\n const keysArray = isArray ? keys : [keys]\n\n if (isArray && keysArray.length === 0) {\n throw new NoKeysPassedToUpdateError()\n }\n\n const callback =\n typeof configOrCallback === `function` ? configOrCallback : maybeCallback!\n const config =\n typeof configOrCallback === `function` ? {} : configOrCallback\n\n // Get the current objects or empty objects if they don't exist\n const currentObjects = keysArray.map((key) => {\n const item = this.state.get(key)\n if (!item) {\n throw new UpdateKeyNotFoundError(key)\n }\n\n return item\n }) as unknown as Array<TInput>\n\n let changesArray\n if (isArray) {\n // Use the proxy to track changes for all objects\n changesArray = withArrayChangeTracking(\n currentObjects,\n callback as (draft: Array<TInput>) => void\n )\n } else {\n const result = withChangeTracking(\n currentObjects[0]!,\n callback as (draft: TInput) => void\n )\n changesArray = [result]\n }\n\n // Create mutations for each object that has changes\n const mutations: Array<\n PendingMutation<\n TOutput,\n `update`,\n CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>\n >\n > = keysArray\n .map((key, index) => {\n const itemChanges = changesArray[index] // User-provided changes for this specific item\n\n // Skip items with no changes\n if (!itemChanges || Object.keys(itemChanges).length === 0) {\n return null\n }\n\n const originalItem = currentObjects[index] as unknown as TOutput\n // Validate the user-provided changes for this item\n const validatedUpdatePayload = this.validateData(\n itemChanges,\n `update`,\n key\n )\n\n // Construct the full modified item by applying the validated update payload to the original item\n const modifiedItem = Object.assign(\n {},\n originalItem,\n validatedUpdatePayload\n )\n\n // Check if the ID of the item is being changed\n const originalItemId = this.config.getKey(originalItem)\n const modifiedItemId = this.config.getKey(modifiedItem)\n\n if (originalItemId !== modifiedItemId) {\n throw new KeyUpdateNotAllowedError(originalItemId, modifiedItemId)\n }\n\n const globalKey = this.generateGlobalKey(modifiedItemId, modifiedItem)\n\n return {\n mutationId: crypto.randomUUID(),\n original: originalItem,\n modified: modifiedItem,\n // Pick the values from modifiedItem based on what's passed in - this is for cases\n // where a schema has default values or transforms. The modified data has the extra\n // default or transformed values but for changes, we just want to show the data that\n // was actually passed in.\n changes: Object.fromEntries(\n Object.keys(itemChanges).map((k) => [\n k,\n modifiedItem[k as keyof typeof modifiedItem],\n ])\n ) as TInput,\n globalKey,\n key,\n metadata: config.metadata as unknown,\n syncMetadata: (state.syncedMetadata.get(key) || {}) as Record<\n string,\n unknown\n >,\n optimistic: config.optimistic ?? true,\n type: `update`,\n createdAt: new Date(),\n updatedAt: new Date(),\n collection: this.collection,\n }\n })\n .filter(Boolean) as Array<\n PendingMutation<\n TOutput,\n `update`,\n CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>\n >\n >\n\n // If no changes were made, return an empty transaction early\n if (mutations.length === 0) {\n const emptyTransaction = createTransaction({\n mutationFn: async () => {},\n })\n // Errors still propagate through tx.isPersisted.promise; suppress the background commit from warning\n emptyTransaction.commit().catch(() => undefined)\n // Schedule cleanup for empty transaction\n state.scheduleTransactionCleanup(emptyTransaction)\n return emptyTransaction\n }\n\n // If an ambient transaction exists, use it\n if (ambientTransaction) {\n ambientTransaction.applyMutations(mutations)\n\n state.transactions.set(ambientTransaction.id, ambientTransaction)\n state.scheduleTransactionCleanup(ambientTransaction)\n state.recomputeOptimisticState(true)\n\n return ambientTransaction\n }\n\n // No need to check for onUpdate handler here as we've already checked at the beginning\n\n // Create a new transaction with a mutation function that calls the onUpdate handler\n const directOpTransaction = createTransaction<TOutput>({\n mutationFn: async (params) => {\n // Call the onUpdate handler with the transaction and collection\n return this.config.onUpdate!({\n transaction:\n params.transaction as unknown as TransactionWithMutations<\n TOutput,\n `update`\n >,\n collection: this.collection as unknown as Collection<TOutput, TKey>,\n })\n },\n })\n\n // Apply mutations to the new transaction\n directOpTransaction.applyMutations(mutations)\n // Errors still hit tx.isPersisted.promise; avoid leaking an unhandled rejection from the fire-and-forget commit\n directOpTransaction.commit().catch(() => undefined)\n\n // Add the transaction to the collection's transactions store\n\n state.transactions.set(directOpTransaction.id, directOpTransaction)\n state.scheduleTransactionCleanup(directOpTransaction)\n state.recomputeOptimisticState(true)\n\n return directOpTransaction\n }\n\n /**\n * Deletes one or more items from the collection\n */\n delete = (\n keys: Array<TKey> | TKey,\n config?: OperationConfig\n ): TransactionType<any> => {\n const state = this.state\n this.lifecycle.validateCollectionUsable(`delete`)\n\n const ambientTransaction = getActiveTransaction()\n\n // If no ambient transaction exists, check for an onDelete handler early\n if (!ambientTransaction && !this.config.onDelete) {\n throw new MissingDeleteHandlerError()\n }\n\n if (Array.isArray(keys) && keys.length === 0) {\n throw new NoKeysPassedToDeleteError()\n }\n\n const keysArray = Array.isArray(keys) ? keys : [keys]\n const mutations: Array<\n PendingMutation<\n TOutput,\n `delete`,\n CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>\n >\n > = []\n\n for (const key of keysArray) {\n if (!this.state.has(key)) {\n throw new DeleteKeyNotFoundError(key)\n }\n const globalKey = this.generateGlobalKey(key, this.state.get(key)!)\n const mutation: PendingMutation<\n TOutput,\n `delete`,\n CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>\n > = {\n mutationId: crypto.randomUUID(),\n original: this.state.get(key)!,\n modified: this.state.get(key)!,\n changes: this.state.get(key)!,\n globalKey,\n key,\n metadata: config?.metadata as unknown,\n syncMetadata: (state.syncedMetadata.get(key) || {}) as Record<\n string,\n unknown\n >,\n optimistic: config?.optimistic ?? true,\n type: `delete`,\n createdAt: new Date(),\n updatedAt: new Date(),\n collection: this.collection,\n }\n\n mutations.push(mutation)\n }\n\n // If an ambient transaction exists, use it\n if (ambientTransaction) {\n ambientTransaction.applyMutations(mutations)\n\n state.transactions.set(ambientTransaction.id, ambientTransaction)\n state.scheduleTransactionCleanup(ambientTransaction)\n state.recomputeOptimisticState(true)\n\n return ambientTransaction\n }\n\n // Create a new transaction with a mutation function that calls the onDelete handler\n const directOpTransaction = createTransaction<TOutput>({\n autoCommit: true,\n mutationFn: async (params) => {\n // Call the onDelete handler with the transaction and collection\n return this.config.onDelete!({\n transaction:\n params.transaction as unknown as TransactionWithMutations<\n TOutput,\n `delete`\n >,\n collection: this.collection as unknown as Collection<TOutput, TKey>,\n })\n },\n })\n\n // Apply mutations to the new transaction\n directOpTransaction.applyMutations(mutations)\n // Errors still reject tx.isPersisted.promise; silence the internal commit promise to prevent test noise\n directOpTransaction.commit().catch(() => undefined)\n\n state.transactions.set(directOpTransaction.id, directOpTransaction)\n state.scheduleTransactionCleanup(directOpTransaction)\n state.recomputeOptimisticState(true)\n\n return directOpTransaction\n }\n}\n"],"names":["config","result"],"mappings":"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"}
|
package/dist/esm/local-only.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseCollectionConfig, CollectionConfig, InferSchemaOutput, UtilsRecord } from './types.js';
|
|
1
|
+
import { BaseCollectionConfig, CollectionConfig, DeleteMutationFn, InferSchemaOutput, InsertMutationFn, PendingMutation, UpdateMutationFn, UtilsRecord } from './types.js';
|
|
2
2
|
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
3
3
|
/**
|
|
4
4
|
* Configuration interface for Local-only collection options
|
|
@@ -14,10 +14,36 @@ export interface LocalOnlyCollectionConfig<T extends object = object, TSchema ex
|
|
|
14
14
|
initialData?: Array<T>;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Local-only collection utilities type
|
|
17
|
+
* Local-only collection utilities type
|
|
18
18
|
*/
|
|
19
19
|
export interface LocalOnlyCollectionUtils extends UtilsRecord {
|
|
20
|
+
/**
|
|
21
|
+
* Accepts mutations from a transaction that belong to this collection and persists them.
|
|
22
|
+
* This should be called in your transaction's mutationFn to persist local-only data.
|
|
23
|
+
*
|
|
24
|
+
* @param transaction - The transaction containing mutations to accept
|
|
25
|
+
* @example
|
|
26
|
+
* const localData = createCollection(localOnlyCollectionOptions({...}))
|
|
27
|
+
*
|
|
28
|
+
* const tx = createTransaction({
|
|
29
|
+
* mutationFn: async ({ transaction }) => {
|
|
30
|
+
* // Make API call first
|
|
31
|
+
* await api.save(...)
|
|
32
|
+
* // Then persist local-only mutations after success
|
|
33
|
+
* localData.utils.acceptMutations(transaction)
|
|
34
|
+
* }
|
|
35
|
+
* })
|
|
36
|
+
*/
|
|
37
|
+
acceptMutations: (transaction: {
|
|
38
|
+
mutations: Array<PendingMutation<Record<string, unknown>>>;
|
|
39
|
+
}) => void;
|
|
20
40
|
}
|
|
41
|
+
type LocalOnlyCollectionOptionsResult<T extends object, TKey extends string | number, TSchema extends StandardSchemaV1 | never = never> = Omit<CollectionConfig<T, TKey, TSchema>, `onInsert` | `onUpdate` | `onDelete`> & {
|
|
42
|
+
onInsert?: InsertMutationFn<T, TKey, LocalOnlyCollectionUtils>;
|
|
43
|
+
onUpdate?: UpdateMutationFn<T, TKey, LocalOnlyCollectionUtils>;
|
|
44
|
+
onDelete?: DeleteMutationFn<T, TKey, LocalOnlyCollectionUtils>;
|
|
45
|
+
utils: LocalOnlyCollectionUtils;
|
|
46
|
+
};
|
|
21
47
|
/**
|
|
22
48
|
* Creates Local-only collection options for use with a standard Collection
|
|
23
49
|
*
|
|
@@ -25,10 +51,16 @@ export interface LocalOnlyCollectionUtils extends UtilsRecord {
|
|
|
25
51
|
* that immediately "syncs" all optimistic changes to the collection, making them permanent.
|
|
26
52
|
* Perfect for local-only data that doesn't need persistence or external synchronization.
|
|
27
53
|
*
|
|
54
|
+
* **Using with Manual Transactions:**
|
|
55
|
+
*
|
|
56
|
+
* For manual transactions, you must call `utils.acceptMutations()` in your transaction's `mutationFn`
|
|
57
|
+
* to persist changes made during `tx.mutate()`. This is necessary because local-only collections
|
|
58
|
+
* don't participate in the standard mutation handler flow for manual transactions.
|
|
59
|
+
*
|
|
28
60
|
* @template T - The schema type if a schema is provided, otherwise the type of items in the collection
|
|
29
61
|
* @template TKey - The type of the key returned by getKey
|
|
30
62
|
* @param config - Configuration options for the Local-only collection
|
|
31
|
-
* @returns Collection options with utilities
|
|
63
|
+
* @returns Collection options with utilities including acceptMutations
|
|
32
64
|
*
|
|
33
65
|
* @example
|
|
34
66
|
* // Basic local-only collection
|
|
@@ -61,16 +93,41 @@ export interface LocalOnlyCollectionUtils extends UtilsRecord {
|
|
|
61
93
|
* },
|
|
62
94
|
* })
|
|
63
95
|
* )
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* // Using with manual transactions
|
|
99
|
+
* const localData = createCollection(
|
|
100
|
+
* localOnlyCollectionOptions({
|
|
101
|
+
* getKey: (item) => item.id,
|
|
102
|
+
* })
|
|
103
|
+
* )
|
|
104
|
+
*
|
|
105
|
+
* const tx = createTransaction({
|
|
106
|
+
* mutationFn: async ({ transaction }) => {
|
|
107
|
+
* // Use local data in API call
|
|
108
|
+
* const localMutations = transaction.mutations.filter(m => m.collection === localData)
|
|
109
|
+
* await api.save({ metadata: localMutations[0]?.modified })
|
|
110
|
+
*
|
|
111
|
+
* // Persist local-only mutations after API success
|
|
112
|
+
* localData.utils.acceptMutations(transaction)
|
|
113
|
+
* }
|
|
114
|
+
* })
|
|
115
|
+
*
|
|
116
|
+
* tx.mutate(() => {
|
|
117
|
+
* localData.insert({ id: 1, data: 'metadata' })
|
|
118
|
+
* apiCollection.insert({ id: 2, data: 'main data' })
|
|
119
|
+
* })
|
|
120
|
+
*
|
|
121
|
+
* await tx.commit()
|
|
64
122
|
*/
|
|
65
123
|
export declare function localOnlyCollectionOptions<T extends StandardSchemaV1, TKey extends string | number = string | number>(config: LocalOnlyCollectionConfig<InferSchemaOutput<T>, T, TKey> & {
|
|
66
124
|
schema: T;
|
|
67
|
-
}):
|
|
68
|
-
utils: LocalOnlyCollectionUtils;
|
|
125
|
+
}): LocalOnlyCollectionOptionsResult<InferSchemaOutput<T>, TKey, T> & {
|
|
69
126
|
schema: T;
|
|
70
127
|
};
|
|
71
128
|
export declare function localOnlyCollectionOptions<T extends object, TKey extends string | number = string | number>(config: LocalOnlyCollectionConfig<T, never, TKey> & {
|
|
72
129
|
schema?: never;
|
|
73
|
-
}):
|
|
74
|
-
utils: LocalOnlyCollectionUtils;
|
|
130
|
+
}): LocalOnlyCollectionOptionsResult<T, TKey> & {
|
|
75
131
|
schema?: never;
|
|
76
132
|
};
|
|
133
|
+
export {};
|
package/dist/esm/local-only.js
CHANGED
|
@@ -25,13 +25,29 @@ function localOnlyCollectionOptions(config) {
|
|
|
25
25
|
syncResult.confirmOperationsSync(params.transaction.mutations);
|
|
26
26
|
return handlerResult;
|
|
27
27
|
};
|
|
28
|
+
const acceptMutations = (transaction) => {
|
|
29
|
+
const collectionMutations = transaction.mutations.filter(
|
|
30
|
+
(m) => (
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
32
|
+
m.collection === syncResult.collection
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
if (collectionMutations.length === 0) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
syncResult.confirmOperationsSync(
|
|
39
|
+
collectionMutations
|
|
40
|
+
);
|
|
41
|
+
};
|
|
28
42
|
return {
|
|
29
43
|
...restConfig,
|
|
30
44
|
sync: syncResult.sync,
|
|
31
45
|
onInsert: wrappedOnInsert,
|
|
32
46
|
onUpdate: wrappedOnUpdate,
|
|
33
47
|
onDelete: wrappedOnDelete,
|
|
34
|
-
utils: {
|
|
48
|
+
utils: {
|
|
49
|
+
acceptMutations
|
|
50
|
+
},
|
|
35
51
|
startSync: true,
|
|
36
52
|
gcTime: 0
|
|
37
53
|
};
|
|
@@ -40,6 +56,7 @@ function createLocalOnlySync(initialData) {
|
|
|
40
56
|
let syncBegin = null;
|
|
41
57
|
let syncWrite = null;
|
|
42
58
|
let syncCommit = null;
|
|
59
|
+
let collection = null;
|
|
43
60
|
const sync = {
|
|
44
61
|
/**
|
|
45
62
|
* Sync function that captures sync parameters and applies initial data
|
|
@@ -51,6 +68,7 @@ function createLocalOnlySync(initialData) {
|
|
|
51
68
|
syncBegin = begin;
|
|
52
69
|
syncWrite = write;
|
|
53
70
|
syncCommit = commit;
|
|
71
|
+
collection = params.collection;
|
|
54
72
|
if (initialData && initialData.length > 0) {
|
|
55
73
|
begin();
|
|
56
74
|
initialData.forEach((item) => {
|
|
@@ -88,7 +106,8 @@ function createLocalOnlySync(initialData) {
|
|
|
88
106
|
};
|
|
89
107
|
return {
|
|
90
108
|
sync,
|
|
91
|
-
confirmOperationsSync
|
|
109
|
+
confirmOperationsSync,
|
|
110
|
+
collection
|
|
92
111
|
};
|
|
93
112
|
}
|
|
94
113
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-only.js","sources":["../../src/local-only.ts"],"sourcesContent":["import type {\n BaseCollectionConfig,\n CollectionConfig,\n DeleteMutationFnParams,\n InferSchemaOutput,\n InsertMutationFnParams,\n OperationType,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"./types\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\n\n/**\n * Configuration interface for Local-only collection options\n * @template T - The type of items in the collection\n * @template TSchema - The schema type for validation\n * @template TKey - The type of the key returned by `getKey`\n */\nexport interface LocalOnlyCollectionConfig<\n T extends object = object,\n TSchema extends StandardSchemaV1 = never,\n TKey extends string | number = string | number,\n> extends Omit<\n BaseCollectionConfig<T, TKey, TSchema, LocalOnlyCollectionUtils>,\n `gcTime` | `startSync`\n > {\n /**\n * Optional initial data to populate the collection with on creation\n * This data will be applied during the initial sync process\n */\n initialData?: Array<T>\n}\n\n/**\n * Local-only collection utilities type (currently empty but matches the pattern)\n */\nexport interface LocalOnlyCollectionUtils extends UtilsRecord {}\n\n/**\n * Creates Local-only collection options for use with a standard Collection\n *\n * This is an in-memory collection that doesn't sync with external sources but uses a loopback sync config\n * that immediately \"syncs\" all optimistic changes to the collection, making them permanent.\n * Perfect for local-only data that doesn't need persistence or external synchronization.\n *\n * @template T - The schema type if a schema is provided, otherwise the type of items in the collection\n * @template TKey - The type of the key returned by getKey\n * @param config - Configuration options for the Local-only collection\n * @returns Collection options with utilities (currently empty but follows the pattern)\n *\n * @example\n * // Basic local-only collection\n * const collection = createCollection(\n * localOnlyCollectionOptions({\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // Local-only collection with initial data\n * const collection = createCollection(\n * localOnlyCollectionOptions({\n * getKey: (item) => item.id,\n * initialData: [\n * { id: 1, name: 'Item 1' },\n * { id: 2, name: 'Item 2' },\n * ],\n * })\n * )\n *\n * @example\n * // Local-only collection with mutation handlers\n * const collection = createCollection(\n * localOnlyCollectionOptions({\n * getKey: (item) => item.id,\n * onInsert: async ({ transaction }) => {\n * console.log('Item inserted:', transaction.mutations[0].modified)\n * // Custom logic after insert\n * },\n * })\n * )\n */\n\n// Overload for when schema is provided\nexport function localOnlyCollectionOptions<\n T extends StandardSchemaV1,\n TKey extends string | number = string | number,\n>(\n config: LocalOnlyCollectionConfig<InferSchemaOutput<T>, T, TKey> & {\n schema: T\n }\n): CollectionConfig<InferSchemaOutput<T>, TKey, T> & {\n utils: LocalOnlyCollectionUtils\n schema: T\n}\n\n// Overload for when no schema is provided\n// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config\nexport function localOnlyCollectionOptions<\n T extends object,\n TKey extends string | number = string | number,\n>(\n config: LocalOnlyCollectionConfig<T, never, TKey> & {\n schema?: never // prohibit schema\n }\n): CollectionConfig<T, TKey> & {\n utils: LocalOnlyCollectionUtils\n schema?: never // no schema in the result\n}\n\nexport function localOnlyCollectionOptions(\n config: LocalOnlyCollectionConfig<any, any, string | number>\n): CollectionConfig<any, string | number, any> & {\n utils: LocalOnlyCollectionUtils\n schema?: StandardSchemaV1\n} {\n const { initialData, onInsert, onUpdate, onDelete, ...restConfig } = config\n\n // Create the sync configuration with transaction confirmation capability\n const syncResult = createLocalOnlySync(initialData)\n\n /**\n * Create wrapper handlers that call user handlers first, then confirm transactions\n * Wraps the user's onInsert handler to also confirm the transaction immediately\n */\n const wrappedOnInsert = async (\n params: InsertMutationFnParams<\n any,\n string | number,\n LocalOnlyCollectionUtils\n >\n ) => {\n // Call user handler first if provided\n let handlerResult\n if (onInsert) {\n handlerResult = (await onInsert(params)) ?? {}\n }\n\n // Then synchronously confirm the transaction by looping through mutations\n syncResult.confirmOperationsSync(params.transaction.mutations)\n\n return handlerResult\n }\n\n /**\n * Wrapper for onUpdate handler that also confirms the transaction immediately\n */\n const wrappedOnUpdate = async (\n params: UpdateMutationFnParams<\n any,\n string | number,\n LocalOnlyCollectionUtils\n >\n ) => {\n // Call user handler first if provided\n let handlerResult\n if (onUpdate) {\n handlerResult = (await onUpdate(params)) ?? {}\n }\n\n // Then synchronously confirm the transaction by looping through mutations\n syncResult.confirmOperationsSync(params.transaction.mutations)\n\n return handlerResult\n }\n\n /**\n * Wrapper for onDelete handler that also confirms the transaction immediately\n */\n const wrappedOnDelete = async (\n params: DeleteMutationFnParams<\n any,\n string | number,\n LocalOnlyCollectionUtils\n >\n ) => {\n // Call user handler first if provided\n let handlerResult\n if (onDelete) {\n handlerResult = (await onDelete(params)) ?? {}\n }\n\n // Then synchronously confirm the transaction by looping through mutations\n syncResult.confirmOperationsSync(params.transaction.mutations)\n\n return handlerResult\n }\n\n return {\n ...restConfig,\n sync: syncResult.sync,\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {} as LocalOnlyCollectionUtils,\n startSync: true,\n gcTime: 0,\n }\n}\n\n/**\n * Internal function to create Local-only sync configuration with transaction confirmation\n *\n * This captures the sync functions and provides synchronous confirmation of operations.\n * It creates a loopback sync that immediately confirms all optimistic operations,\n * making them permanent in the collection.\n *\n * @param initialData - Optional array of initial items to populate the collection\n * @returns Object with sync configuration and confirmOperationsSync function\n */\nfunction createLocalOnlySync<T extends object, TKey extends string | number>(\n initialData?: Array<T>\n) {\n // Capture sync functions for transaction confirmation\n let syncBegin: (() => void) | null = null\n let syncWrite: ((message: { type: OperationType; value: T }) => void) | null =\n null\n let syncCommit: (() => void) | null = null\n\n const sync: SyncConfig<T, TKey> = {\n /**\n * Sync function that captures sync parameters and applies initial data\n * @param params - Sync parameters containing begin, write, and commit functions\n * @returns Unsubscribe function (empty since no ongoing sync is needed)\n */\n sync: (params) => {\n const { begin, write, commit, markReady } = params\n\n // Capture sync functions for later use by confirmOperationsSync\n syncBegin = begin\n syncWrite = write\n syncCommit = commit\n\n // Apply initial data if provided\n if (initialData && initialData.length > 0) {\n begin()\n initialData.forEach((item) => {\n write({\n type: `insert`,\n value: item,\n })\n })\n commit()\n }\n\n // Mark collection as ready since local-only collections are immediately ready\n markReady()\n\n // Return empty unsubscribe function - no ongoing sync needed\n return () => {}\n },\n /**\n * Get sync metadata - returns empty object for local-only collections\n * @returns Empty metadata object\n */\n getSyncMetadata: () => ({}),\n }\n\n /**\n * Synchronously confirms optimistic operations by immediately writing through sync\n *\n * This loops through transaction mutations and applies them to move from optimistic to synced state.\n * It's called after user handlers to make optimistic changes permanent.\n *\n * @param mutations - Array of mutation objects from the transaction\n */\n const confirmOperationsSync = (mutations: Array<any>) => {\n if (!syncBegin || !syncWrite || !syncCommit) {\n return // Sync not initialized yet, which is fine\n }\n\n // Immediately write back through sync interface\n syncBegin()\n mutations.forEach((mutation) => {\n if (syncWrite) {\n syncWrite({\n type: mutation.type,\n value: mutation.modified,\n })\n }\n })\n syncCommit()\n }\n\n return {\n sync,\n confirmOperationsSync,\n }\n}\n"],"names":[],"mappings":"AA+GO,SAAS,2BACd,QAIA;AACA,QAAM,EAAE,aAAa,UAAU,UAAU,UAAU,GAAG,eAAe;AAGrE,QAAM,aAAa,oBAAoB,WAAW;AAMlD,QAAM,kBAAkB,OACtB,WAKG;AAEH,QAAI;AACJ,QAAI,UAAU;AACZ,sBAAiB,MAAM,SAAS,MAAM,KAAM,CAAA;AAAA,IAC9C;AAGA,eAAW,sBAAsB,OAAO,YAAY,SAAS;AAE7D,WAAO;AAAA,EACT;AAKA,QAAM,kBAAkB,OACtB,WAKG;AAEH,QAAI;AACJ,QAAI,UAAU;AACZ,sBAAiB,MAAM,SAAS,MAAM,KAAM,CAAA;AAAA,IAC9C;AAGA,eAAW,sBAAsB,OAAO,YAAY,SAAS;AAE7D,WAAO;AAAA,EACT;AAKA,QAAM,kBAAkB,OACtB,WAKG;AAEH,QAAI;AACJ,QAAI,UAAU;AACZ,sBAAiB,MAAM,SAAS,MAAM,KAAM,CAAA;AAAA,IAC9C;AAGA,eAAW,sBAAsB,OAAO,YAAY,SAAS;AAE7D,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,MAAM,WAAW;AAAA,IACjB,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,OAAO,CAAA;AAAA,IACP,WAAW;AAAA,IACX,QAAQ;AAAA,EAAA;AAEZ;AAYA,SAAS,oBACP,aACA;AAEA,MAAI,YAAiC;AACrC,MAAI,YACF;AACF,MAAI,aAAkC;AAEtC,QAAM,OAA4B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMhC,MAAM,CAAC,WAAW;AAChB,YAAM,EAAE,OAAO,OAAO,QAAQ,cAAc;AAG5C,kBAAY;AACZ,kBAAY;AACZ,mBAAa;AAGb,UAAI,eAAe,YAAY,SAAS,GAAG;AACzC,cAAA;AACA,oBAAY,QAAQ,CAAC,SAAS;AAC5B,gBAAM;AAAA,YACJ,MAAM;AAAA,YACN,OAAO;AAAA,UAAA,CACR;AAAA,QACH,CAAC;AACD,eAAA;AAAA,MACF;AAGA,gBAAA;AAGA,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,iBAAiB,OAAO,CAAA;AAAA,EAAC;AAW3B,QAAM,wBAAwB,CAAC,cAA0B;AACvD,QAAI,CAAC,aAAa,CAAC,aAAa,CAAC,YAAY;AAC3C;AAAA,IACF;AAGA,cAAA;AACA,cAAU,QAAQ,CAAC,aAAa;AAC9B,UAAI,WAAW;AACb,kBAAU;AAAA,UACR,MAAM,SAAS;AAAA,UACf,OAAO,SAAS;AAAA,QAAA,CACjB;AAAA,MACH;AAAA,IACF,CAAC;AACD,eAAA;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EAAA;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"local-only.js","sources":["../../src/local-only.ts"],"sourcesContent":["import type {\n BaseCollectionConfig,\n CollectionConfig,\n DeleteMutationFn,\n DeleteMutationFnParams,\n InferSchemaOutput,\n InsertMutationFn,\n InsertMutationFnParams,\n OperationType,\n PendingMutation,\n SyncConfig,\n UpdateMutationFn,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"./types\"\nimport type { Collection } from \"./collection/index\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\n\n/**\n * Configuration interface for Local-only collection options\n * @template T - The type of items in the collection\n * @template TSchema - The schema type for validation\n * @template TKey - The type of the key returned by `getKey`\n */\nexport interface LocalOnlyCollectionConfig<\n T extends object = object,\n TSchema extends StandardSchemaV1 = never,\n TKey extends string | number = string | number,\n> extends Omit<\n BaseCollectionConfig<T, TKey, TSchema, LocalOnlyCollectionUtils>,\n `gcTime` | `startSync`\n > {\n /**\n * Optional initial data to populate the collection with on creation\n * This data will be applied during the initial sync process\n */\n initialData?: Array<T>\n}\n\n/**\n * Local-only collection utilities type\n */\nexport interface LocalOnlyCollectionUtils extends UtilsRecord {\n /**\n * Accepts mutations from a transaction that belong to this collection and persists them.\n * This should be called in your transaction's mutationFn to persist local-only data.\n *\n * @param transaction - The transaction containing mutations to accept\n * @example\n * const localData = createCollection(localOnlyCollectionOptions({...}))\n *\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Make API call first\n * await api.save(...)\n * // Then persist local-only mutations after success\n * localData.utils.acceptMutations(transaction)\n * }\n * })\n */\n acceptMutations: (transaction: {\n mutations: Array<PendingMutation<Record<string, unknown>>>\n }) => void\n}\n\ntype LocalOnlyCollectionOptionsResult<\n T extends object,\n TKey extends string | number,\n TSchema extends StandardSchemaV1 | never = never,\n> = Omit<\n CollectionConfig<T, TKey, TSchema>,\n `onInsert` | `onUpdate` | `onDelete`\n> & {\n onInsert?: InsertMutationFn<T, TKey, LocalOnlyCollectionUtils>\n onUpdate?: UpdateMutationFn<T, TKey, LocalOnlyCollectionUtils>\n onDelete?: DeleteMutationFn<T, TKey, LocalOnlyCollectionUtils>\n utils: LocalOnlyCollectionUtils\n}\n\n/**\n * Creates Local-only collection options for use with a standard Collection\n *\n * This is an in-memory collection that doesn't sync with external sources but uses a loopback sync config\n * that immediately \"syncs\" all optimistic changes to the collection, making them permanent.\n * Perfect for local-only data that doesn't need persistence or external synchronization.\n *\n * **Using with Manual Transactions:**\n *\n * For manual transactions, you must call `utils.acceptMutations()` in your transaction's `mutationFn`\n * to persist changes made during `tx.mutate()`. This is necessary because local-only collections\n * don't participate in the standard mutation handler flow for manual transactions.\n *\n * @template T - The schema type if a schema is provided, otherwise the type of items in the collection\n * @template TKey - The type of the key returned by getKey\n * @param config - Configuration options for the Local-only collection\n * @returns Collection options with utilities including acceptMutations\n *\n * @example\n * // Basic local-only collection\n * const collection = createCollection(\n * localOnlyCollectionOptions({\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // Local-only collection with initial data\n * const collection = createCollection(\n * localOnlyCollectionOptions({\n * getKey: (item) => item.id,\n * initialData: [\n * { id: 1, name: 'Item 1' },\n * { id: 2, name: 'Item 2' },\n * ],\n * })\n * )\n *\n * @example\n * // Local-only collection with mutation handlers\n * const collection = createCollection(\n * localOnlyCollectionOptions({\n * getKey: (item) => item.id,\n * onInsert: async ({ transaction }) => {\n * console.log('Item inserted:', transaction.mutations[0].modified)\n * // Custom logic after insert\n * },\n * })\n * )\n *\n * @example\n * // Using with manual transactions\n * const localData = createCollection(\n * localOnlyCollectionOptions({\n * getKey: (item) => item.id,\n * })\n * )\n *\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Use local data in API call\n * const localMutations = transaction.mutations.filter(m => m.collection === localData)\n * await api.save({ metadata: localMutations[0]?.modified })\n *\n * // Persist local-only mutations after API success\n * localData.utils.acceptMutations(transaction)\n * }\n * })\n *\n * tx.mutate(() => {\n * localData.insert({ id: 1, data: 'metadata' })\n * apiCollection.insert({ id: 2, data: 'main data' })\n * })\n *\n * await tx.commit()\n */\n\n// Overload for when schema is provided\nexport function localOnlyCollectionOptions<\n T extends StandardSchemaV1,\n TKey extends string | number = string | number,\n>(\n config: LocalOnlyCollectionConfig<InferSchemaOutput<T>, T, TKey> & {\n schema: T\n }\n): LocalOnlyCollectionOptionsResult<InferSchemaOutput<T>, TKey, T> & {\n schema: T\n}\n\n// Overload for when no schema is provided\n// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config\nexport function localOnlyCollectionOptions<\n T extends object,\n TKey extends string | number = string | number,\n>(\n config: LocalOnlyCollectionConfig<T, never, TKey> & {\n schema?: never // prohibit schema\n }\n): LocalOnlyCollectionOptionsResult<T, TKey> & {\n schema?: never // no schema in the result\n}\n\nexport function localOnlyCollectionOptions<\n T extends object = object,\n TSchema extends StandardSchemaV1 = never,\n TKey extends string | number = string | number,\n>(\n config: LocalOnlyCollectionConfig<T, TSchema, TKey>\n): LocalOnlyCollectionOptionsResult<T, TKey, TSchema> & {\n schema?: StandardSchemaV1\n} {\n const { initialData, onInsert, onUpdate, onDelete, ...restConfig } = config\n\n // Create the sync configuration with transaction confirmation capability\n const syncResult = createLocalOnlySync(initialData)\n\n /**\n * Create wrapper handlers that call user handlers first, then confirm transactions\n * Wraps the user's onInsert handler to also confirm the transaction immediately\n */\n const wrappedOnInsert = async (\n params: InsertMutationFnParams<T, TKey, LocalOnlyCollectionUtils>\n ) => {\n // Call user handler first if provided\n let handlerResult\n if (onInsert) {\n handlerResult = (await onInsert(params)) ?? {}\n }\n\n // Then synchronously confirm the transaction by looping through mutations\n syncResult.confirmOperationsSync(params.transaction.mutations)\n\n return handlerResult\n }\n\n /**\n * Wrapper for onUpdate handler that also confirms the transaction immediately\n */\n const wrappedOnUpdate = async (\n params: UpdateMutationFnParams<T, TKey, LocalOnlyCollectionUtils>\n ) => {\n // Call user handler first if provided\n let handlerResult\n if (onUpdate) {\n handlerResult = (await onUpdate(params)) ?? {}\n }\n\n // Then synchronously confirm the transaction by looping through mutations\n syncResult.confirmOperationsSync(params.transaction.mutations)\n\n return handlerResult\n }\n\n /**\n * Wrapper for onDelete handler that also confirms the transaction immediately\n */\n const wrappedOnDelete = async (\n params: DeleteMutationFnParams<T, TKey, LocalOnlyCollectionUtils>\n ) => {\n // Call user handler first if provided\n let handlerResult\n if (onDelete) {\n handlerResult = (await onDelete(params)) ?? {}\n }\n\n // Then synchronously confirm the transaction by looping through mutations\n syncResult.confirmOperationsSync(params.transaction.mutations)\n\n return handlerResult\n }\n\n /**\n * Accepts mutations from a transaction that belong to this collection and persists them\n */\n const acceptMutations = (transaction: {\n mutations: Array<PendingMutation<Record<string, unknown>>>\n }) => {\n // Filter mutations that belong to this collection\n const collectionMutations = transaction.mutations.filter(\n (m) =>\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n m.collection === syncResult.collection\n )\n\n if (collectionMutations.length === 0) {\n return\n }\n\n // Persist the mutations through sync\n syncResult.confirmOperationsSync(\n collectionMutations as Array<PendingMutation<T>>\n )\n }\n\n return {\n ...restConfig,\n sync: syncResult.sync,\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n acceptMutations,\n } as LocalOnlyCollectionUtils,\n startSync: true,\n gcTime: 0,\n }\n}\n\n/**\n * Internal function to create Local-only sync configuration with transaction confirmation\n *\n * This captures the sync functions and provides synchronous confirmation of operations.\n * It creates a loopback sync that immediately confirms all optimistic operations,\n * making them permanent in the collection.\n *\n * @param initialData - Optional array of initial items to populate the collection\n * @returns Object with sync configuration and confirmOperationsSync function\n */\nfunction createLocalOnlySync<T extends object, TKey extends string | number>(\n initialData?: Array<T>\n) {\n // Capture sync functions and collection for transaction confirmation\n let syncBegin: (() => void) | null = null\n let syncWrite: ((message: { type: OperationType; value: T }) => void) | null =\n null\n let syncCommit: (() => void) | null = null\n let collection: Collection<T, TKey, LocalOnlyCollectionUtils> | null = null\n\n const sync: SyncConfig<T, TKey> = {\n /**\n * Sync function that captures sync parameters and applies initial data\n * @param params - Sync parameters containing begin, write, and commit functions\n * @returns Unsubscribe function (empty since no ongoing sync is needed)\n */\n sync: (params) => {\n const { begin, write, commit, markReady } = params\n\n // Capture sync functions and collection for later use\n syncBegin = begin\n syncWrite = write\n syncCommit = commit\n collection = params.collection\n\n // Apply initial data if provided\n if (initialData && initialData.length > 0) {\n begin()\n initialData.forEach((item) => {\n write({\n type: `insert`,\n value: item,\n })\n })\n commit()\n }\n\n // Mark collection as ready since local-only collections are immediately ready\n markReady()\n\n // Return empty unsubscribe function - no ongoing sync needed\n return () => {}\n },\n /**\n * Get sync metadata - returns empty object for local-only collections\n * @returns Empty metadata object\n */\n getSyncMetadata: () => ({}),\n }\n\n /**\n * Synchronously confirms optimistic operations by immediately writing through sync\n *\n * This loops through transaction mutations and applies them to move from optimistic to synced state.\n * It's called after user handlers to make optimistic changes permanent.\n *\n * @param mutations - Array of mutation objects from the transaction\n */\n const confirmOperationsSync = (mutations: Array<PendingMutation<T>>) => {\n if (!syncBegin || !syncWrite || !syncCommit) {\n return // Sync not initialized yet, which is fine\n }\n\n // Immediately write back through sync interface\n syncBegin()\n mutations.forEach((mutation) => {\n if (syncWrite) {\n syncWrite({\n type: mutation.type,\n value: mutation.modified,\n })\n }\n })\n syncCommit()\n }\n\n return {\n sync,\n confirmOperationsSync,\n collection,\n }\n}\n"],"names":[],"mappings":"AAqLO,SAAS,2BAKd,QAGA;AACA,QAAM,EAAE,aAAa,UAAU,UAAU,UAAU,GAAG,eAAe;AAGrE,QAAM,aAAa,oBAAoB,WAAW;AAMlD,QAAM,kBAAkB,OACtB,WACG;AAEH,QAAI;AACJ,QAAI,UAAU;AACZ,sBAAiB,MAAM,SAAS,MAAM,KAAM,CAAA;AAAA,IAC9C;AAGA,eAAW,sBAAsB,OAAO,YAAY,SAAS;AAE7D,WAAO;AAAA,EACT;AAKA,QAAM,kBAAkB,OACtB,WACG;AAEH,QAAI;AACJ,QAAI,UAAU;AACZ,sBAAiB,MAAM,SAAS,MAAM,KAAM,CAAA;AAAA,IAC9C;AAGA,eAAW,sBAAsB,OAAO,YAAY,SAAS;AAE7D,WAAO;AAAA,EACT;AAKA,QAAM,kBAAkB,OACtB,WACG;AAEH,QAAI;AACJ,QAAI,UAAU;AACZ,sBAAiB,MAAM,SAAS,MAAM,KAAM,CAAA;AAAA,IAC9C;AAGA,eAAW,sBAAsB,OAAO,YAAY,SAAS;AAE7D,WAAO;AAAA,EACT;AAKA,QAAM,kBAAkB,CAAC,gBAEnB;AAEJ,UAAM,sBAAsB,YAAY,UAAU;AAAA,MAChD,CAAC;AAAA;AAAA,QAEC,EAAE,eAAe,WAAW;AAAA;AAAA,IAAA;AAGhC,QAAI,oBAAoB,WAAW,GAAG;AACpC;AAAA,IACF;AAGA,eAAW;AAAA,MACT;AAAA,IAAA;AAAA,EAEJ;AAEA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,MAAM,WAAW;AAAA,IACjB,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,OAAO;AAAA,MACL;AAAA,IAAA;AAAA,IAEF,WAAW;AAAA,IACX,QAAQ;AAAA,EAAA;AAEZ;AAYA,SAAS,oBACP,aACA;AAEA,MAAI,YAAiC;AACrC,MAAI,YACF;AACF,MAAI,aAAkC;AACtC,MAAI,aAAmE;AAEvE,QAAM,OAA4B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMhC,MAAM,CAAC,WAAW;AAChB,YAAM,EAAE,OAAO,OAAO,QAAQ,cAAc;AAG5C,kBAAY;AACZ,kBAAY;AACZ,mBAAa;AACb,mBAAa,OAAO;AAGpB,UAAI,eAAe,YAAY,SAAS,GAAG;AACzC,cAAA;AACA,oBAAY,QAAQ,CAAC,SAAS;AAC5B,gBAAM;AAAA,YACJ,MAAM;AAAA,YACN,OAAO;AAAA,UAAA,CACR;AAAA,QACH,CAAC;AACD,eAAA;AAAA,MACF;AAGA,gBAAA;AAGA,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,iBAAiB,OAAO,CAAA;AAAA,EAAC;AAW3B,QAAM,wBAAwB,CAAC,cAAyC;AACtE,QAAI,CAAC,aAAa,CAAC,aAAa,CAAC,YAAY;AAC3C;AAAA,IACF;AAGA,cAAA;AACA,cAAU,QAAQ,CAAC,aAAa;AAC9B,UAAI,WAAW;AACb,kBAAU;AAAA,UACR,MAAM,SAAS;AAAA,UACf,OAAO,SAAS;AAAA,QAAA,CACjB;AAAA,MACH;AAAA,IACF,CAAC;AACD,eAAA;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseCollectionConfig, CollectionConfig, InferSchemaOutput, UtilsRecord } from './types.js';
|
|
1
|
+
import { BaseCollectionConfig, CollectionConfig, InferSchemaOutput, PendingMutation, UtilsRecord } from './types.js';
|
|
2
2
|
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
3
3
|
/**
|
|
4
4
|
* Storage API interface - subset of DOM Storage that we need
|
|
@@ -47,6 +47,26 @@ export type GetStorageSizeFn = () => number;
|
|
|
47
47
|
export interface LocalStorageCollectionUtils extends UtilsRecord {
|
|
48
48
|
clearStorage: ClearStorageFn;
|
|
49
49
|
getStorageSize: GetStorageSizeFn;
|
|
50
|
+
/**
|
|
51
|
+
* Accepts mutations from a transaction that belong to this collection and persists them to localStorage.
|
|
52
|
+
* This should be called in your transaction's mutationFn to persist local-storage data.
|
|
53
|
+
*
|
|
54
|
+
* @param transaction - The transaction containing mutations to accept
|
|
55
|
+
* @example
|
|
56
|
+
* const localSettings = createCollection(localStorageCollectionOptions({...}))
|
|
57
|
+
*
|
|
58
|
+
* const tx = createTransaction({
|
|
59
|
+
* mutationFn: async ({ transaction }) => {
|
|
60
|
+
* // Make API call first
|
|
61
|
+
* await api.save(...)
|
|
62
|
+
* // Then persist local-storage mutations after success
|
|
63
|
+
* localSettings.utils.acceptMutations(transaction)
|
|
64
|
+
* }
|
|
65
|
+
* })
|
|
66
|
+
*/
|
|
67
|
+
acceptMutations: (transaction: {
|
|
68
|
+
mutations: Array<PendingMutation<Record<string, unknown>>>;
|
|
69
|
+
}) => void;
|
|
50
70
|
}
|
|
51
71
|
/**
|
|
52
72
|
* Creates localStorage collection options for use with a standard Collection
|
|
@@ -54,11 +74,17 @@ export interface LocalStorageCollectionUtils extends UtilsRecord {
|
|
|
54
74
|
* This function creates a collection that persists data to localStorage/sessionStorage
|
|
55
75
|
* and synchronizes changes across browser tabs using storage events.
|
|
56
76
|
*
|
|
77
|
+
* **Using with Manual Transactions:**
|
|
78
|
+
*
|
|
79
|
+
* For manual transactions, you must call `utils.acceptMutations()` in your transaction's `mutationFn`
|
|
80
|
+
* to persist changes made during `tx.mutate()`. This is necessary because local-storage collections
|
|
81
|
+
* don't participate in the standard mutation handler flow for manual transactions.
|
|
82
|
+
*
|
|
57
83
|
* @template TExplicit - The explicit type of items in the collection (highest priority)
|
|
58
84
|
* @template TSchema - The schema type for validation and type inference (second priority)
|
|
59
85
|
* @template TFallback - The fallback type if no explicit or schema type is provided
|
|
60
86
|
* @param config - Configuration options for the localStorage collection
|
|
61
|
-
* @returns Collection options with utilities including clearStorage and
|
|
87
|
+
* @returns Collection options with utilities including clearStorage, getStorageSize, and acceptMutations
|
|
62
88
|
*
|
|
63
89
|
* @example
|
|
64
90
|
* // Basic localStorage collection
|
|
@@ -90,6 +116,33 @@ export interface LocalStorageCollectionUtils extends UtilsRecord {
|
|
|
90
116
|
* },
|
|
91
117
|
* })
|
|
92
118
|
* )
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* // Using with manual transactions
|
|
122
|
+
* const localSettings = createCollection(
|
|
123
|
+
* localStorageCollectionOptions({
|
|
124
|
+
* storageKey: 'user-settings',
|
|
125
|
+
* getKey: (item) => item.id,
|
|
126
|
+
* })
|
|
127
|
+
* )
|
|
128
|
+
*
|
|
129
|
+
* const tx = createTransaction({
|
|
130
|
+
* mutationFn: async ({ transaction }) => {
|
|
131
|
+
* // Use settings data in API call
|
|
132
|
+
* const settingsMutations = transaction.mutations.filter(m => m.collection === localSettings)
|
|
133
|
+
* await api.updateUserProfile({ settings: settingsMutations[0]?.modified })
|
|
134
|
+
*
|
|
135
|
+
* // Persist local-storage mutations after API success
|
|
136
|
+
* localSettings.utils.acceptMutations(transaction)
|
|
137
|
+
* }
|
|
138
|
+
* })
|
|
139
|
+
*
|
|
140
|
+
* tx.mutate(() => {
|
|
141
|
+
* localSettings.insert({ id: 'theme', value: 'dark' })
|
|
142
|
+
* apiCollection.insert({ id: 2, data: 'profile data' })
|
|
143
|
+
* })
|
|
144
|
+
*
|
|
145
|
+
* await tx.commit()
|
|
93
146
|
*/
|
|
94
147
|
export declare function localStorageCollectionOptions<T extends StandardSchemaV1, TKey extends string | number = string | number>(config: LocalStorageCollectionConfig<InferSchemaOutput<T>, T, TKey> & {
|
|
95
148
|
schema: T;
|