@tanstack/db 0.4.5 → 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/change-events.cjs +1 -1
- package/dist/cjs/collection/change-events.cjs.map +1 -1
- package/dist/cjs/collection/change-events.d.cts +1 -1
- package/dist/cjs/collection/index.cjs +11 -0
- package/dist/cjs/collection/index.cjs.map +1 -1
- package/dist/cjs/collection/index.d.cts +8 -1
- package/dist/cjs/collection/lifecycle.cjs +4 -1
- package/dist/cjs/collection/lifecycle.cjs.map +1 -1
- package/dist/cjs/collection/mutations.cjs +4 -4
- package/dist/cjs/collection/mutations.cjs.map +1 -1
- package/dist/cjs/collection/subscription.cjs +21 -1
- package/dist/cjs/collection/subscription.cjs.map +1 -1
- package/dist/cjs/collection/subscription.d.cts +4 -3
- package/dist/cjs/collection/sync.cjs +94 -71
- package/dist/cjs/collection/sync.cjs.map +1 -1
- package/dist/cjs/collection/sync.d.cts +9 -1
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +2 -0
- package/dist/cjs/indexes/auto-index.cjs +4 -1
- package/dist/cjs/indexes/auto-index.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/cjs/query/compiler/expressions.cjs +19 -0
- package/dist/cjs/query/compiler/expressions.cjs.map +1 -1
- package/dist/cjs/query/compiler/expressions.d.cts +2 -1
- package/dist/cjs/query/compiler/order-by.cjs +2 -1
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +2 -1
- package/dist/cjs/query/live/collection-subscriber.cjs +18 -8
- package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.d.cts +1 -0
- package/dist/cjs/types.d.cts +11 -1
- package/dist/esm/collection/change-events.d.ts +1 -1
- package/dist/esm/collection/change-events.js +1 -1
- package/dist/esm/collection/change-events.js.map +1 -1
- package/dist/esm/collection/index.d.ts +8 -1
- package/dist/esm/collection/index.js +11 -0
- package/dist/esm/collection/index.js.map +1 -1
- package/dist/esm/collection/lifecycle.js +4 -1
- package/dist/esm/collection/lifecycle.js.map +1 -1
- package/dist/esm/collection/mutations.js +4 -4
- package/dist/esm/collection/mutations.js.map +1 -1
- package/dist/esm/collection/subscription.d.ts +4 -3
- package/dist/esm/collection/subscription.js +22 -2
- package/dist/esm/collection/subscription.js.map +1 -1
- package/dist/esm/collection/sync.d.ts +9 -1
- package/dist/esm/collection/sync.js +94 -71
- package/dist/esm/collection/sync.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/indexes/auto-index.js +4 -1
- package/dist/esm/indexes/auto-index.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/dist/esm/query/compiler/expressions.d.ts +2 -1
- package/dist/esm/query/compiler/expressions.js +19 -0
- package/dist/esm/query/compiler/expressions.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +2 -1
- package/dist/esm/query/compiler/order-by.js +2 -1
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/live/collection-subscriber.d.ts +1 -0
- package/dist/esm/query/live/collection-subscriber.js +19 -9
- package/dist/esm/query/live/collection-subscriber.js.map +1 -1
- package/dist/esm/types.d.ts +11 -1
- package/package.json +1 -1
- package/src/collection/change-events.ts +5 -2
- package/src/collection/index.ts +13 -0
- package/src/collection/lifecycle.ts +4 -1
- package/src/collection/mutations.ts +8 -4
- package/src/collection/subscription.ts +34 -4
- package/src/collection/sync.ts +147 -110
- package/src/index.ts +5 -0
- package/src/indexes/auto-index.ts +4 -1
- package/src/local-only.ts +119 -30
- package/src/local-storage.ts +170 -5
- package/src/query/compiler/expressions.ts +26 -1
- package/src/query/compiler/order-by.ts +3 -1
- package/src/query/live/collection-subscriber.ts +31 -10
- package/src/types.ts +13 -1
package/src/collection/sync.ts
CHANGED
|
@@ -9,7 +9,13 @@ import {
|
|
|
9
9
|
} from "../errors"
|
|
10
10
|
import { deepEquals } from "../utils"
|
|
11
11
|
import type { StandardSchemaV1 } from "@standard-schema/spec"
|
|
12
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
ChangeMessage,
|
|
14
|
+
CleanupFn,
|
|
15
|
+
CollectionConfig,
|
|
16
|
+
OnLoadMoreOptions,
|
|
17
|
+
SyncConfigRes,
|
|
18
|
+
} from "../types"
|
|
13
19
|
import type { CollectionImpl } from "./index.js"
|
|
14
20
|
import type { CollectionStateManager } from "./state"
|
|
15
21
|
import type { CollectionLifecycleManager } from "./lifecycle"
|
|
@@ -28,6 +34,9 @@ export class CollectionSyncManager<
|
|
|
28
34
|
|
|
29
35
|
public preloadPromise: Promise<void> | null = null
|
|
30
36
|
public syncCleanupFn: (() => void) | null = null
|
|
37
|
+
public syncOnLoadMoreFn:
|
|
38
|
+
| ((options: OnLoadMoreOptions) => void | Promise<void>)
|
|
39
|
+
| null = null
|
|
31
40
|
|
|
32
41
|
/**
|
|
33
42
|
* Creates a new CollectionSyncManager instance
|
|
@@ -52,7 +61,6 @@ export class CollectionSyncManager<
|
|
|
52
61
|
* This is called when the collection is first accessed or preloaded
|
|
53
62
|
*/
|
|
54
63
|
public startSync(): void {
|
|
55
|
-
const state = this.state
|
|
56
64
|
if (
|
|
57
65
|
this.lifecycle.status !== `idle` &&
|
|
58
66
|
this.lifecycle.status !== `cleaned-up`
|
|
@@ -63,120 +71,125 @@ export class CollectionSyncManager<
|
|
|
63
71
|
this.lifecycle.setStatus(`loading`)
|
|
64
72
|
|
|
65
73
|
try {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
state.pendingSyncedTransactions
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
insertingIntoExistingSynced &&
|
|
99
|
-
!hasPendingDeleteForKey &&
|
|
100
|
-
!isTruncateTransaction
|
|
101
|
-
) {
|
|
102
|
-
const existingValue = state.syncedData.get(key)
|
|
74
|
+
const syncRes = normalizeSyncFnResult(
|
|
75
|
+
this.config.sync.sync({
|
|
76
|
+
collection: this.collection,
|
|
77
|
+
begin: () => {
|
|
78
|
+
this.state.pendingSyncedTransactions.push({
|
|
79
|
+
committed: false,
|
|
80
|
+
operations: [],
|
|
81
|
+
deletedKeys: new Set(),
|
|
82
|
+
})
|
|
83
|
+
},
|
|
84
|
+
write: (messageWithoutKey: Omit<ChangeMessage<TOutput>, `key`>) => {
|
|
85
|
+
const pendingTransaction =
|
|
86
|
+
this.state.pendingSyncedTransactions[
|
|
87
|
+
this.state.pendingSyncedTransactions.length - 1
|
|
88
|
+
]
|
|
89
|
+
if (!pendingTransaction) {
|
|
90
|
+
throw new NoPendingSyncTransactionWriteError()
|
|
91
|
+
}
|
|
92
|
+
if (pendingTransaction.committed) {
|
|
93
|
+
throw new SyncTransactionAlreadyCommittedWriteError()
|
|
94
|
+
}
|
|
95
|
+
const key = this.config.getKey(messageWithoutKey.value)
|
|
96
|
+
|
|
97
|
+
let messageType = messageWithoutKey.type
|
|
98
|
+
|
|
99
|
+
// Check if an item with this key already exists when inserting
|
|
100
|
+
if (messageWithoutKey.type === `insert`) {
|
|
101
|
+
const insertingIntoExistingSynced = this.state.syncedData.has(key)
|
|
102
|
+
const hasPendingDeleteForKey =
|
|
103
|
+
pendingTransaction.deletedKeys.has(key)
|
|
104
|
+
const isTruncateTransaction = pendingTransaction.truncate === true
|
|
105
|
+
// Allow insert after truncate in the same transaction even if it existed in syncedData
|
|
103
106
|
if (
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
insertingIntoExistingSynced &&
|
|
108
|
+
!hasPendingDeleteForKey &&
|
|
109
|
+
!isTruncateTransaction
|
|
106
110
|
) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
const existingValue = this.state.syncedData.get(key)
|
|
112
|
+
if (
|
|
113
|
+
existingValue !== undefined &&
|
|
114
|
+
deepEquals(existingValue, messageWithoutKey.value)
|
|
115
|
+
) {
|
|
116
|
+
// The "insert" is an echo of a value we already have locally.
|
|
117
|
+
// Treat it as an update so we preserve optimistic intent without
|
|
118
|
+
// throwing a duplicate-key error during reconciliation.
|
|
119
|
+
messageType = `update`
|
|
120
|
+
} else {
|
|
121
|
+
throw new DuplicateKeySyncError(key, this.id)
|
|
122
|
+
}
|
|
113
123
|
}
|
|
114
124
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
125
|
+
|
|
126
|
+
const message: ChangeMessage<TOutput> = {
|
|
127
|
+
...messageWithoutKey,
|
|
128
|
+
type: messageType,
|
|
129
|
+
key,
|
|
130
|
+
}
|
|
131
|
+
pendingTransaction.operations.push(message)
|
|
132
|
+
|
|
133
|
+
if (messageType === `delete`) {
|
|
134
|
+
pendingTransaction.deletedKeys.add(key)
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
commit: () => {
|
|
138
|
+
const pendingTransaction =
|
|
139
|
+
this.state.pendingSyncedTransactions[
|
|
140
|
+
this.state.pendingSyncedTransactions.length - 1
|
|
141
|
+
]
|
|
142
|
+
if (!pendingTransaction) {
|
|
143
|
+
throw new NoPendingSyncTransactionCommitError()
|
|
144
|
+
}
|
|
145
|
+
if (pendingTransaction.committed) {
|
|
146
|
+
throw new SyncTransactionAlreadyCommittedError()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
pendingTransaction.committed = true
|
|
150
|
+
|
|
151
|
+
// Update status to initialCommit when transitioning from loading
|
|
152
|
+
// This indicates we're in the process of committing the first transaction
|
|
153
|
+
if (this.lifecycle.status === `loading`) {
|
|
154
|
+
this.lifecycle.setStatus(`initialCommit`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.state.commitPendingTransactions()
|
|
158
|
+
},
|
|
159
|
+
markReady: () => {
|
|
160
|
+
this.lifecycle.markReady()
|
|
161
|
+
},
|
|
162
|
+
truncate: () => {
|
|
163
|
+
const pendingTransaction =
|
|
164
|
+
this.state.pendingSyncedTransactions[
|
|
165
|
+
this.state.pendingSyncedTransactions.length - 1
|
|
166
|
+
]
|
|
167
|
+
if (!pendingTransaction) {
|
|
168
|
+
throw new NoPendingSyncTransactionWriteError()
|
|
169
|
+
}
|
|
170
|
+
if (pendingTransaction.committed) {
|
|
171
|
+
throw new SyncTransactionAlreadyCommittedWriteError()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Clear all operations from the current transaction
|
|
175
|
+
pendingTransaction.operations = []
|
|
176
|
+
pendingTransaction.deletedKeys.clear()
|
|
177
|
+
|
|
178
|
+
// Mark the transaction as a truncate operation. During commit, this triggers:
|
|
179
|
+
// - Delete events for all previously synced keys (excluding optimistic-deleted keys)
|
|
180
|
+
// - Clearing of syncedData/syncedMetadata
|
|
181
|
+
// - Subsequent synced ops applied on the fresh base
|
|
182
|
+
// - Finally, optimistic mutations re-applied on top (single batch)
|
|
183
|
+
pendingTransaction.truncate = true
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
)
|
|
177
187
|
|
|
178
188
|
// Store cleanup function if provided
|
|
179
|
-
this.syncCleanupFn =
|
|
189
|
+
this.syncCleanupFn = syncRes?.cleanup ?? null
|
|
190
|
+
|
|
191
|
+
// Store onLoadMore function if provided
|
|
192
|
+
this.syncOnLoadMoreFn = syncRes?.onLoadMore ?? null
|
|
180
193
|
} catch (error) {
|
|
181
194
|
this.lifecycle.setStatus(`error`)
|
|
182
195
|
throw error
|
|
@@ -225,6 +238,18 @@ export class CollectionSyncManager<
|
|
|
225
238
|
return this.preloadPromise
|
|
226
239
|
}
|
|
227
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Requests the sync layer to load more data.
|
|
243
|
+
* @param options Options to control what data is being loaded
|
|
244
|
+
* @returns If data loading is asynchronous, this method returns a promise that resolves when the data is loaded.
|
|
245
|
+
* If data loading is synchronous, the data is loaded when the method returns.
|
|
246
|
+
*/
|
|
247
|
+
public syncMore(options: OnLoadMoreOptions): void | Promise<void> {
|
|
248
|
+
if (this.syncOnLoadMoreFn) {
|
|
249
|
+
return this.syncOnLoadMoreFn(options)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
228
253
|
public cleanup(): void {
|
|
229
254
|
try {
|
|
230
255
|
if (this.syncCleanupFn) {
|
|
@@ -248,3 +273,15 @@ export class CollectionSyncManager<
|
|
|
248
273
|
this.preloadPromise = null
|
|
249
274
|
}
|
|
250
275
|
}
|
|
276
|
+
|
|
277
|
+
function normalizeSyncFnResult(result: void | CleanupFn | SyncConfigRes) {
|
|
278
|
+
if (typeof result === `function`) {
|
|
279
|
+
return { cleanup: result }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (typeof result === `object`) {
|
|
283
|
+
return result
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return undefined
|
|
287
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
// Re-export all public APIs
|
|
2
|
+
// Re-export IR types under their own namespace
|
|
3
|
+
// because custom collections need access to the IR types
|
|
4
|
+
import * as IR from "./query/ir.js"
|
|
5
|
+
|
|
2
6
|
export * from "./collection/index.js"
|
|
3
7
|
export * from "./SortedMap"
|
|
4
8
|
export * from "./transactions"
|
|
@@ -18,3 +22,4 @@ export { type IndexOptions } from "./indexes/index-options.js"
|
|
|
18
22
|
|
|
19
23
|
// Re-export some stuff explicitly to ensure the type & value is exported
|
|
20
24
|
export type { Collection } from "./collection/index.js"
|
|
25
|
+
export { IR }
|
|
@@ -58,7 +58,10 @@ export function ensureIndexForField<
|
|
|
58
58
|
options: compareFn ? { compareFn, compareOptions } : {},
|
|
59
59
|
})
|
|
60
60
|
} catch (error) {
|
|
61
|
-
console.warn(
|
|
61
|
+
console.warn(
|
|
62
|
+
`${collection.id ? `[${collection.id}] ` : ``}Failed to create auto-index for field "${fieldName}":`,
|
|
63
|
+
error
|
|
64
|
+
)
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
|
package/src/local-only.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
BaseCollectionConfig,
|
|
3
3
|
CollectionConfig,
|
|
4
|
+
DeleteMutationFn,
|
|
4
5
|
DeleteMutationFnParams,
|
|
5
6
|
InferSchemaOutput,
|
|
7
|
+
InsertMutationFn,
|
|
6
8
|
InsertMutationFnParams,
|
|
7
9
|
OperationType,
|
|
10
|
+
PendingMutation,
|
|
8
11
|
SyncConfig,
|
|
12
|
+
UpdateMutationFn,
|
|
9
13
|
UpdateMutationFnParams,
|
|
10
14
|
UtilsRecord,
|
|
11
15
|
} from "./types"
|
|
16
|
+
import type { Collection } from "./collection/index"
|
|
12
17
|
import type { StandardSchemaV1 } from "@standard-schema/spec"
|
|
13
18
|
|
|
14
19
|
/**
|
|
@@ -33,9 +38,44 @@ export interface LocalOnlyCollectionConfig<
|
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
/**
|
|
36
|
-
* Local-only collection utilities type
|
|
41
|
+
* Local-only collection utilities type
|
|
37
42
|
*/
|
|
38
|
-
export interface LocalOnlyCollectionUtils extends UtilsRecord {
|
|
43
|
+
export interface LocalOnlyCollectionUtils extends UtilsRecord {
|
|
44
|
+
/**
|
|
45
|
+
* Accepts mutations from a transaction that belong to this collection and persists them.
|
|
46
|
+
* This should be called in your transaction's mutationFn to persist local-only data.
|
|
47
|
+
*
|
|
48
|
+
* @param transaction - The transaction containing mutations to accept
|
|
49
|
+
* @example
|
|
50
|
+
* const localData = createCollection(localOnlyCollectionOptions({...}))
|
|
51
|
+
*
|
|
52
|
+
* const tx = createTransaction({
|
|
53
|
+
* mutationFn: async ({ transaction }) => {
|
|
54
|
+
* // Make API call first
|
|
55
|
+
* await api.save(...)
|
|
56
|
+
* // Then persist local-only mutations after success
|
|
57
|
+
* localData.utils.acceptMutations(transaction)
|
|
58
|
+
* }
|
|
59
|
+
* })
|
|
60
|
+
*/
|
|
61
|
+
acceptMutations: (transaction: {
|
|
62
|
+
mutations: Array<PendingMutation<Record<string, unknown>>>
|
|
63
|
+
}) => void
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type LocalOnlyCollectionOptionsResult<
|
|
67
|
+
T extends object,
|
|
68
|
+
TKey extends string | number,
|
|
69
|
+
TSchema extends StandardSchemaV1 | never = never,
|
|
70
|
+
> = Omit<
|
|
71
|
+
CollectionConfig<T, TKey, TSchema>,
|
|
72
|
+
`onInsert` | `onUpdate` | `onDelete`
|
|
73
|
+
> & {
|
|
74
|
+
onInsert?: InsertMutationFn<T, TKey, LocalOnlyCollectionUtils>
|
|
75
|
+
onUpdate?: UpdateMutationFn<T, TKey, LocalOnlyCollectionUtils>
|
|
76
|
+
onDelete?: DeleteMutationFn<T, TKey, LocalOnlyCollectionUtils>
|
|
77
|
+
utils: LocalOnlyCollectionUtils
|
|
78
|
+
}
|
|
39
79
|
|
|
40
80
|
/**
|
|
41
81
|
* Creates Local-only collection options for use with a standard Collection
|
|
@@ -44,10 +84,16 @@ export interface LocalOnlyCollectionUtils extends UtilsRecord {}
|
|
|
44
84
|
* that immediately "syncs" all optimistic changes to the collection, making them permanent.
|
|
45
85
|
* Perfect for local-only data that doesn't need persistence or external synchronization.
|
|
46
86
|
*
|
|
87
|
+
* **Using with Manual Transactions:**
|
|
88
|
+
*
|
|
89
|
+
* For manual transactions, you must call `utils.acceptMutations()` in your transaction's `mutationFn`
|
|
90
|
+
* to persist changes made during `tx.mutate()`. This is necessary because local-only collections
|
|
91
|
+
* don't participate in the standard mutation handler flow for manual transactions.
|
|
92
|
+
*
|
|
47
93
|
* @template T - The schema type if a schema is provided, otherwise the type of items in the collection
|
|
48
94
|
* @template TKey - The type of the key returned by getKey
|
|
49
95
|
* @param config - Configuration options for the Local-only collection
|
|
50
|
-
* @returns Collection options with utilities
|
|
96
|
+
* @returns Collection options with utilities including acceptMutations
|
|
51
97
|
*
|
|
52
98
|
* @example
|
|
53
99
|
* // Basic local-only collection
|
|
@@ -80,6 +126,32 @@ export interface LocalOnlyCollectionUtils extends UtilsRecord {}
|
|
|
80
126
|
* },
|
|
81
127
|
* })
|
|
82
128
|
* )
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* // Using with manual transactions
|
|
132
|
+
* const localData = createCollection(
|
|
133
|
+
* localOnlyCollectionOptions({
|
|
134
|
+
* getKey: (item) => item.id,
|
|
135
|
+
* })
|
|
136
|
+
* )
|
|
137
|
+
*
|
|
138
|
+
* const tx = createTransaction({
|
|
139
|
+
* mutationFn: async ({ transaction }) => {
|
|
140
|
+
* // Use local data in API call
|
|
141
|
+
* const localMutations = transaction.mutations.filter(m => m.collection === localData)
|
|
142
|
+
* await api.save({ metadata: localMutations[0]?.modified })
|
|
143
|
+
*
|
|
144
|
+
* // Persist local-only mutations after API success
|
|
145
|
+
* localData.utils.acceptMutations(transaction)
|
|
146
|
+
* }
|
|
147
|
+
* })
|
|
148
|
+
*
|
|
149
|
+
* tx.mutate(() => {
|
|
150
|
+
* localData.insert({ id: 1, data: 'metadata' })
|
|
151
|
+
* apiCollection.insert({ id: 2, data: 'main data' })
|
|
152
|
+
* })
|
|
153
|
+
*
|
|
154
|
+
* await tx.commit()
|
|
83
155
|
*/
|
|
84
156
|
|
|
85
157
|
// Overload for when schema is provided
|
|
@@ -90,8 +162,7 @@ export function localOnlyCollectionOptions<
|
|
|
90
162
|
config: LocalOnlyCollectionConfig<InferSchemaOutput<T>, T, TKey> & {
|
|
91
163
|
schema: T
|
|
92
164
|
}
|
|
93
|
-
):
|
|
94
|
-
utils: LocalOnlyCollectionUtils
|
|
165
|
+
): LocalOnlyCollectionOptionsResult<InferSchemaOutput<T>, TKey, T> & {
|
|
95
166
|
schema: T
|
|
96
167
|
}
|
|
97
168
|
|
|
@@ -104,15 +175,17 @@ export function localOnlyCollectionOptions<
|
|
|
104
175
|
config: LocalOnlyCollectionConfig<T, never, TKey> & {
|
|
105
176
|
schema?: never // prohibit schema
|
|
106
177
|
}
|
|
107
|
-
):
|
|
108
|
-
utils: LocalOnlyCollectionUtils
|
|
178
|
+
): LocalOnlyCollectionOptionsResult<T, TKey> & {
|
|
109
179
|
schema?: never // no schema in the result
|
|
110
180
|
}
|
|
111
181
|
|
|
112
|
-
export function localOnlyCollectionOptions
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
182
|
+
export function localOnlyCollectionOptions<
|
|
183
|
+
T extends object = object,
|
|
184
|
+
TSchema extends StandardSchemaV1 = never,
|
|
185
|
+
TKey extends string | number = string | number,
|
|
186
|
+
>(
|
|
187
|
+
config: LocalOnlyCollectionConfig<T, TSchema, TKey>
|
|
188
|
+
): LocalOnlyCollectionOptionsResult<T, TKey, TSchema> & {
|
|
116
189
|
schema?: StandardSchemaV1
|
|
117
190
|
} {
|
|
118
191
|
const { initialData, onInsert, onUpdate, onDelete, ...restConfig } = config
|
|
@@ -125,11 +198,7 @@ export function localOnlyCollectionOptions(
|
|
|
125
198
|
* Wraps the user's onInsert handler to also confirm the transaction immediately
|
|
126
199
|
*/
|
|
127
200
|
const wrappedOnInsert = async (
|
|
128
|
-
params: InsertMutationFnParams<
|
|
129
|
-
any,
|
|
130
|
-
string | number,
|
|
131
|
-
LocalOnlyCollectionUtils
|
|
132
|
-
>
|
|
201
|
+
params: InsertMutationFnParams<T, TKey, LocalOnlyCollectionUtils>
|
|
133
202
|
) => {
|
|
134
203
|
// Call user handler first if provided
|
|
135
204
|
let handlerResult
|
|
@@ -147,11 +216,7 @@ export function localOnlyCollectionOptions(
|
|
|
147
216
|
* Wrapper for onUpdate handler that also confirms the transaction immediately
|
|
148
217
|
*/
|
|
149
218
|
const wrappedOnUpdate = async (
|
|
150
|
-
params: UpdateMutationFnParams<
|
|
151
|
-
any,
|
|
152
|
-
string | number,
|
|
153
|
-
LocalOnlyCollectionUtils
|
|
154
|
-
>
|
|
219
|
+
params: UpdateMutationFnParams<T, TKey, LocalOnlyCollectionUtils>
|
|
155
220
|
) => {
|
|
156
221
|
// Call user handler first if provided
|
|
157
222
|
let handlerResult
|
|
@@ -169,11 +234,7 @@ export function localOnlyCollectionOptions(
|
|
|
169
234
|
* Wrapper for onDelete handler that also confirms the transaction immediately
|
|
170
235
|
*/
|
|
171
236
|
const wrappedOnDelete = async (
|
|
172
|
-
params: DeleteMutationFnParams<
|
|
173
|
-
any,
|
|
174
|
-
string | number,
|
|
175
|
-
LocalOnlyCollectionUtils
|
|
176
|
-
>
|
|
237
|
+
params: DeleteMutationFnParams<T, TKey, LocalOnlyCollectionUtils>
|
|
177
238
|
) => {
|
|
178
239
|
// Call user handler first if provided
|
|
179
240
|
let handlerResult
|
|
@@ -187,13 +248,38 @@ export function localOnlyCollectionOptions(
|
|
|
187
248
|
return handlerResult
|
|
188
249
|
}
|
|
189
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Accepts mutations from a transaction that belong to this collection and persists them
|
|
253
|
+
*/
|
|
254
|
+
const acceptMutations = (transaction: {
|
|
255
|
+
mutations: Array<PendingMutation<Record<string, unknown>>>
|
|
256
|
+
}) => {
|
|
257
|
+
// Filter mutations that belong to this collection
|
|
258
|
+
const collectionMutations = transaction.mutations.filter(
|
|
259
|
+
(m) =>
|
|
260
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
261
|
+
m.collection === syncResult.collection
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if (collectionMutations.length === 0) {
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Persist the mutations through sync
|
|
269
|
+
syncResult.confirmOperationsSync(
|
|
270
|
+
collectionMutations as Array<PendingMutation<T>>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
190
274
|
return {
|
|
191
275
|
...restConfig,
|
|
192
276
|
sync: syncResult.sync,
|
|
193
277
|
onInsert: wrappedOnInsert,
|
|
194
278
|
onUpdate: wrappedOnUpdate,
|
|
195
279
|
onDelete: wrappedOnDelete,
|
|
196
|
-
utils: {
|
|
280
|
+
utils: {
|
|
281
|
+
acceptMutations,
|
|
282
|
+
} as LocalOnlyCollectionUtils,
|
|
197
283
|
startSync: true,
|
|
198
284
|
gcTime: 0,
|
|
199
285
|
}
|
|
@@ -212,11 +298,12 @@ export function localOnlyCollectionOptions(
|
|
|
212
298
|
function createLocalOnlySync<T extends object, TKey extends string | number>(
|
|
213
299
|
initialData?: Array<T>
|
|
214
300
|
) {
|
|
215
|
-
// Capture sync functions for transaction confirmation
|
|
301
|
+
// Capture sync functions and collection for transaction confirmation
|
|
216
302
|
let syncBegin: (() => void) | null = null
|
|
217
303
|
let syncWrite: ((message: { type: OperationType; value: T }) => void) | null =
|
|
218
304
|
null
|
|
219
305
|
let syncCommit: (() => void) | null = null
|
|
306
|
+
let collection: Collection<T, TKey, LocalOnlyCollectionUtils> | null = null
|
|
220
307
|
|
|
221
308
|
const sync: SyncConfig<T, TKey> = {
|
|
222
309
|
/**
|
|
@@ -227,10 +314,11 @@ function createLocalOnlySync<T extends object, TKey extends string | number>(
|
|
|
227
314
|
sync: (params) => {
|
|
228
315
|
const { begin, write, commit, markReady } = params
|
|
229
316
|
|
|
230
|
-
// Capture sync functions for later use
|
|
317
|
+
// Capture sync functions and collection for later use
|
|
231
318
|
syncBegin = begin
|
|
232
319
|
syncWrite = write
|
|
233
320
|
syncCommit = commit
|
|
321
|
+
collection = params.collection
|
|
234
322
|
|
|
235
323
|
// Apply initial data if provided
|
|
236
324
|
if (initialData && initialData.length > 0) {
|
|
@@ -265,7 +353,7 @@ function createLocalOnlySync<T extends object, TKey extends string | number>(
|
|
|
265
353
|
*
|
|
266
354
|
* @param mutations - Array of mutation objects from the transaction
|
|
267
355
|
*/
|
|
268
|
-
const confirmOperationsSync = (mutations: Array<
|
|
356
|
+
const confirmOperationsSync = (mutations: Array<PendingMutation<T>>) => {
|
|
269
357
|
if (!syncBegin || !syncWrite || !syncCommit) {
|
|
270
358
|
return // Sync not initialized yet, which is fine
|
|
271
359
|
}
|
|
@@ -286,5 +374,6 @@ function createLocalOnlySync<T extends object, TKey extends string | number>(
|
|
|
286
374
|
return {
|
|
287
375
|
sync,
|
|
288
376
|
confirmOperationsSync,
|
|
377
|
+
collection,
|
|
289
378
|
}
|
|
290
379
|
}
|