@tanstack/db 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/collection.cjs +441 -284
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +103 -30
- package/dist/cjs/proxy.cjs +2 -2
- package/dist/cjs/proxy.cjs.map +1 -1
- package/dist/cjs/query/compiled-query.cjs +23 -37
- package/dist/cjs/query/compiled-query.cjs.map +1 -1
- package/dist/cjs/query/compiled-query.d.cts +2 -2
- package/dist/cjs/query/order-by.cjs +41 -38
- package/dist/cjs/query/order-by.cjs.map +1 -1
- package/dist/cjs/query/query-builder.cjs.map +1 -1
- package/dist/cjs/query/query-builder.d.cts +2 -2
- package/dist/cjs/query/schema.d.cts +5 -4
- package/dist/cjs/transactions.cjs +7 -6
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/transactions.d.cts +9 -9
- package/dist/cjs/types.d.cts +28 -22
- package/dist/esm/collection.d.ts +103 -30
- package/dist/esm/collection.js +442 -285
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/proxy.js +2 -2
- package/dist/esm/proxy.js.map +1 -1
- package/dist/esm/query/compiled-query.d.ts +2 -2
- package/dist/esm/query/compiled-query.js +23 -37
- package/dist/esm/query/compiled-query.js.map +1 -1
- package/dist/esm/query/order-by.js +41 -38
- package/dist/esm/query/order-by.js.map +1 -1
- package/dist/esm/query/query-builder.d.ts +2 -2
- package/dist/esm/query/query-builder.js.map +1 -1
- package/dist/esm/query/schema.d.ts +5 -4
- package/dist/esm/transactions.d.ts +9 -9
- package/dist/esm/transactions.js +7 -6
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +28 -22
- package/package.json +2 -2
- package/src/collection.ts +624 -372
- package/src/proxy.ts +2 -2
- package/src/query/compiled-query.ts +26 -37
- package/src/query/order-by.ts +69 -67
- package/src/query/query-builder.ts +4 -3
- package/src/query/schema.ts +13 -3
- package/src/transactions.ts +24 -22
- package/src/types.ts +44 -22
package/src/proxy.ts
CHANGED
|
@@ -642,7 +642,7 @@ export function createChangeProxy<
|
|
|
642
642
|
return value
|
|
643
643
|
},
|
|
644
644
|
|
|
645
|
-
set(
|
|
645
|
+
set(_sobj, prop, value) {
|
|
646
646
|
const currentValue = changeTracker.copy_[prop as keyof T]
|
|
647
647
|
debugLog(
|
|
648
648
|
`set called for property ${String(prop)}, current:`,
|
|
@@ -716,7 +716,7 @@ export function createChangeProxy<
|
|
|
716
716
|
return true
|
|
717
717
|
},
|
|
718
718
|
|
|
719
|
-
defineProperty(
|
|
719
|
+
defineProperty(_ptarget, prop, descriptor) {
|
|
720
720
|
// const result = Reflect.defineProperty(
|
|
721
721
|
// changeTracker.copy_,
|
|
722
722
|
// prop,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { D2, MessageType, MultiSet, output } from "@electric-sql/d2ts"
|
|
2
|
-
import { Effect, batch } from "@tanstack/store"
|
|
3
2
|
import { createCollection } from "../collection.js"
|
|
4
3
|
import { compileQueryPipeline } from "./pipeline-compiler.js"
|
|
5
4
|
import type { Collection } from "../collection.js"
|
|
@@ -27,7 +26,7 @@ export class CompiledQuery<TResults extends object = Record<string, unknown>> {
|
|
|
27
26
|
private resultCollection: Collection<TResults>
|
|
28
27
|
public state: `compiled` | `running` | `stopped` = `compiled`
|
|
29
28
|
private version = 0
|
|
30
|
-
private
|
|
29
|
+
private unsubscribeCallbacks: Array<() => void> = []
|
|
31
30
|
|
|
32
31
|
constructor(queryBuilder: QueryBuilder<Context<Schema>>) {
|
|
33
32
|
const query = queryBuilder._query
|
|
@@ -100,7 +99,7 @@ export class CompiledQuery<TResults extends object = Record<string, unknown>> {
|
|
|
100
99
|
this.inputs = inputs
|
|
101
100
|
this.resultCollection = createCollection<TResults>({
|
|
102
101
|
id: crypto.randomUUID(), // TODO: remove when we don't require any more
|
|
103
|
-
|
|
102
|
+
getKey: (val: unknown) => {
|
|
104
103
|
return (val as any)._key
|
|
105
104
|
},
|
|
106
105
|
sync: {
|
|
@@ -116,12 +115,12 @@ export class CompiledQuery<TResults extends object = Record<string, unknown>> {
|
|
|
116
115
|
private sendChangesToInput(
|
|
117
116
|
inputKey: string,
|
|
118
117
|
changes: Array<ChangeMessage>,
|
|
119
|
-
|
|
118
|
+
getKey: (item: ChangeMessage[`value`]) => any
|
|
120
119
|
) {
|
|
121
120
|
const input = this.inputs[inputKey]!
|
|
122
121
|
const multiSetArray: MultiSetArray<unknown> = []
|
|
123
122
|
for (const change of changes) {
|
|
124
|
-
const key =
|
|
123
|
+
const key = getKey(change.value)
|
|
125
124
|
if (change.type === `insert`) {
|
|
126
125
|
multiSetArray.push([[key, change.value], 1])
|
|
127
126
|
} else if (change.type === `update`) {
|
|
@@ -161,39 +160,29 @@ export class CompiledQuery<TResults extends object = Record<string, unknown>> {
|
|
|
161
160
|
throw new Error(`Query is stopped`)
|
|
162
161
|
}
|
|
163
162
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
})
|
|
172
|
-
this.incrementVersion()
|
|
173
|
-
this.sendFrontierToAllInputs()
|
|
174
|
-
this.runGraph()
|
|
163
|
+
// Send initial state
|
|
164
|
+
Object.entries(this.inputCollections).forEach(([key, collection]) => {
|
|
165
|
+
this.sendChangesToInput(
|
|
166
|
+
key,
|
|
167
|
+
collection.currentStateAsChanges(),
|
|
168
|
+
collection.config.getKey
|
|
169
|
+
)
|
|
175
170
|
})
|
|
171
|
+
this.incrementVersion()
|
|
172
|
+
this.sendFrontierToAllInputs()
|
|
173
|
+
this.runGraph()
|
|
174
|
+
|
|
175
|
+
// Subscribe to changes
|
|
176
|
+
Object.entries(this.inputCollections).forEach(([key, collection]) => {
|
|
177
|
+
const unsubscribe = collection.subscribeChanges((changes) => {
|
|
178
|
+
this.sendChangesToInput(key, changes, collection.config.getKey)
|
|
179
|
+
this.incrementVersion()
|
|
180
|
+
this.sendFrontierToAllInputs()
|
|
181
|
+
this.runGraph()
|
|
182
|
+
})
|
|
176
183
|
|
|
177
|
-
|
|
178
|
-
fn: () => {
|
|
179
|
-
batch(() => {
|
|
180
|
-
Object.entries(this.inputCollections).forEach(([key, collection]) => {
|
|
181
|
-
this.sendChangesToInput(
|
|
182
|
-
key,
|
|
183
|
-
collection.derivedChanges.state,
|
|
184
|
-
collection.config.getId
|
|
185
|
-
)
|
|
186
|
-
})
|
|
187
|
-
this.incrementVersion()
|
|
188
|
-
this.sendFrontierToAllInputs()
|
|
189
|
-
this.runGraph()
|
|
190
|
-
})
|
|
191
|
-
},
|
|
192
|
-
deps: Object.values(this.inputCollections).map(
|
|
193
|
-
(collection) => collection.derivedChanges
|
|
194
|
-
),
|
|
184
|
+
this.unsubscribeCallbacks.push(unsubscribe)
|
|
195
185
|
})
|
|
196
|
-
this.unsubscribeEffect = changeEffect.mount()
|
|
197
186
|
|
|
198
187
|
this.state = `running`
|
|
199
188
|
return () => {
|
|
@@ -202,8 +191,8 @@ export class CompiledQuery<TResults extends object = Record<string, unknown>> {
|
|
|
202
191
|
}
|
|
203
192
|
|
|
204
193
|
stop() {
|
|
205
|
-
this.
|
|
206
|
-
this.
|
|
194
|
+
this.unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe())
|
|
195
|
+
this.unsubscribeCallbacks = []
|
|
207
196
|
this.state = `stopped`
|
|
208
197
|
}
|
|
209
198
|
}
|
package/src/query/order-by.ts
CHANGED
|
@@ -13,6 +13,13 @@ import type {
|
|
|
13
13
|
NamespacedRow,
|
|
14
14
|
} from "../types"
|
|
15
15
|
|
|
16
|
+
type OrderByItem = {
|
|
17
|
+
operand: ConditionOperand
|
|
18
|
+
direction: `asc` | `desc`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type OrderByItems = Array<OrderByItem>
|
|
22
|
+
|
|
16
23
|
export function processOrderBy(
|
|
17
24
|
resultPipeline: NamespacedAndKeyedStream,
|
|
18
25
|
query: Query,
|
|
@@ -41,10 +48,7 @@ export function processOrderBy(
|
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
// Normalize orderBy to an array of objects
|
|
44
|
-
const orderByItems:
|
|
45
|
-
operand: ConditionOperand
|
|
46
|
-
direction: `asc` | `desc`
|
|
47
|
-
}> = []
|
|
51
|
+
const orderByItems: OrderByItems = []
|
|
48
52
|
|
|
49
53
|
if (typeof query.orderBy === `string`) {
|
|
50
54
|
// Handle string format: '@column'
|
|
@@ -84,22 +88,13 @@ export function processOrderBy(
|
|
|
84
88
|
const valueExtractor = (namespacedRow: NamespacedRow) => {
|
|
85
89
|
// For multiple orderBy columns, create a composite key
|
|
86
90
|
if (orderByItems.length > 1) {
|
|
87
|
-
return orderByItems.map((item) =>
|
|
88
|
-
|
|
91
|
+
return orderByItems.map((item) =>
|
|
92
|
+
evaluateOperandOnNamespacedRow(
|
|
89
93
|
namespacedRow,
|
|
90
94
|
item.operand,
|
|
91
95
|
mainTableAlias
|
|
92
96
|
)
|
|
93
|
-
|
|
94
|
-
// Reverse the value for 'desc' ordering
|
|
95
|
-
return item.direction === `desc` && typeof val === `number`
|
|
96
|
-
? -val
|
|
97
|
-
: item.direction === `desc` && typeof val === `string`
|
|
98
|
-
? String.fromCharCode(
|
|
99
|
-
...[...val].map((c) => 0xffff - c.charCodeAt(0))
|
|
100
|
-
)
|
|
101
|
-
: val
|
|
102
|
-
})
|
|
97
|
+
)
|
|
103
98
|
} else if (orderByItems.length === 1) {
|
|
104
99
|
// For a single orderBy column, use the value directly
|
|
105
100
|
const item = orderByItems[0]
|
|
@@ -108,66 +103,24 @@ export function processOrderBy(
|
|
|
108
103
|
item!.operand,
|
|
109
104
|
mainTableAlias
|
|
110
105
|
)
|
|
111
|
-
|
|
112
|
-
// Reverse the value for 'desc' ordering
|
|
113
|
-
return item!.direction === `desc` && typeof val === `number`
|
|
114
|
-
? -val
|
|
115
|
-
: item!.direction === `desc` && typeof val === `string`
|
|
116
|
-
? String.fromCharCode(
|
|
117
|
-
...[...val].map((c) => 0xffff - c.charCodeAt(0))
|
|
118
|
-
)
|
|
119
|
-
: val
|
|
106
|
+
return val
|
|
120
107
|
}
|
|
121
108
|
|
|
122
109
|
// Default case - no ordering
|
|
123
110
|
return null
|
|
124
111
|
}
|
|
125
112
|
|
|
126
|
-
const
|
|
127
|
-
// if a and b are both
|
|
128
|
-
if (typeof a === `number` && typeof b === `number`) {
|
|
129
|
-
return a - b
|
|
130
|
-
}
|
|
131
|
-
// if a and b are both strings, compare them lexicographically
|
|
113
|
+
const ascComparator = (a: any, b: any): number => {
|
|
114
|
+
// if a and b are both strings, compare them based on locale
|
|
132
115
|
if (typeof a === `string` && typeof b === `string`) {
|
|
133
116
|
return a.localeCompare(b)
|
|
134
117
|
}
|
|
135
|
-
// if a and b are both booleans, compare them
|
|
136
|
-
if (typeof a === `boolean` && typeof b === `boolean`) {
|
|
137
|
-
return a === b ? 0 : a ? 1 : -1
|
|
138
|
-
}
|
|
139
|
-
// if a and b are both dates, compare them
|
|
140
|
-
if (a instanceof Date && b instanceof Date) {
|
|
141
|
-
return a.getTime() - b.getTime()
|
|
142
|
-
}
|
|
143
|
-
// if a and b are both null, return 0
|
|
144
|
-
if (a === null || b === null) {
|
|
145
|
-
return 0
|
|
146
|
-
}
|
|
147
118
|
|
|
148
119
|
// if a and b are both arrays, compare them element by element
|
|
149
120
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
150
121
|
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
151
|
-
// Get the values from the array
|
|
152
|
-
const aVal = a[i]
|
|
153
|
-
const bVal = b[i]
|
|
154
|
-
|
|
155
122
|
// Compare the values
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (typeof aVal === `boolean` && typeof bVal === `boolean`) {
|
|
159
|
-
// Special handling for booleans - false comes before true
|
|
160
|
-
result = aVal === bVal ? 0 : aVal ? 1 : -1
|
|
161
|
-
} else if (typeof aVal === `number` && typeof bVal === `number`) {
|
|
162
|
-
// Numeric comparison
|
|
163
|
-
result = aVal - bVal
|
|
164
|
-
} else if (typeof aVal === `string` && typeof bVal === `string`) {
|
|
165
|
-
// String comparison
|
|
166
|
-
result = aVal.localeCompare(bVal)
|
|
167
|
-
} else {
|
|
168
|
-
// Default comparison using the general comparator
|
|
169
|
-
result = comparator(aVal, bVal)
|
|
170
|
-
}
|
|
123
|
+
const result = ascComparator(a[i], b[i])
|
|
171
124
|
|
|
172
125
|
if (result !== 0) {
|
|
173
126
|
return result
|
|
@@ -176,13 +129,62 @@ export function processOrderBy(
|
|
|
176
129
|
// All elements are equal up to the minimum length
|
|
177
130
|
return a.length - b.length
|
|
178
131
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
132
|
+
|
|
133
|
+
// If at least one of the values is an object then we don't really know how to meaningfully compare them
|
|
134
|
+
// therefore we turn them into strings and compare those
|
|
135
|
+
// There are 2 exceptions:
|
|
136
|
+
// 1) if both objects are dates then we can compare them
|
|
137
|
+
// 2) if either object is nullish then we can't call toString on it
|
|
138
|
+
const bothObjects = typeof a === `object` && typeof b === `object`
|
|
139
|
+
const bothDates = a instanceof Date && b instanceof Date
|
|
140
|
+
const notNull = a !== null && b !== null
|
|
141
|
+
if (bothObjects && !bothDates && notNull) {
|
|
142
|
+
// Every object should support `toString`
|
|
143
|
+
return a.toString().localeCompare(b.toString())
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (a < b) return -1
|
|
147
|
+
if (a > b) return 1
|
|
148
|
+
return 0
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const descComparator = (a: unknown, b: unknown): number => {
|
|
152
|
+
return ascComparator(b, a)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Create a multi-property comparator that respects the order and direction of each property
|
|
156
|
+
const makeComparator = (orderByProps: OrderByItems) => {
|
|
157
|
+
return (a: unknown, b: unknown) => {
|
|
158
|
+
// If we're comparing arrays (multiple properties), compare each property in order
|
|
159
|
+
if (orderByProps.length > 1) {
|
|
160
|
+
// `a` and `b` must be arrays since `orderByItems.length > 1`
|
|
161
|
+
// hence the extracted values must be arrays
|
|
162
|
+
const arrayA = a as Array<unknown>
|
|
163
|
+
const arrayB = b as Array<unknown>
|
|
164
|
+
for (let i = 0; i < orderByProps.length; i++) {
|
|
165
|
+
const direction = orderByProps[i]!.direction
|
|
166
|
+
const compareFn =
|
|
167
|
+
direction === `desc` ? descComparator : ascComparator
|
|
168
|
+
const result = compareFn(arrayA[i], arrayB[i])
|
|
169
|
+
if (result !== 0) {
|
|
170
|
+
return result
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// should normally always be 0 because
|
|
174
|
+
// both values are extracted based on orderByItems
|
|
175
|
+
return arrayA.length - arrayB.length
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Single property comparison
|
|
179
|
+
if (orderByProps.length === 1) {
|
|
180
|
+
const direction = orderByProps[0]!.direction
|
|
181
|
+
return direction === `desc` ? descComparator(a, b) : ascComparator(a, b)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return ascComparator(a, b)
|
|
182
185
|
}
|
|
183
|
-
// Fallback to string comparison for all other cases
|
|
184
|
-
return (a as any).toString().localeCompare((b as any).toString())
|
|
185
186
|
}
|
|
187
|
+
const comparator = makeComparator(orderByItems)
|
|
186
188
|
|
|
187
189
|
// Apply the appropriate orderBy operator based on whether an ORDER_INDEX column is requested
|
|
188
190
|
if (hasOrderIndexColumn) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Collection } from "../collection"
|
|
2
2
|
import type {
|
|
3
3
|
Comparator,
|
|
4
|
+
ComparatorValue,
|
|
4
5
|
Condition,
|
|
5
6
|
From,
|
|
6
7
|
JoinClause,
|
|
@@ -287,10 +288,10 @@ export class BaseQueryBuilder<TContext extends Context<Schema>> {
|
|
|
287
288
|
/**
|
|
288
289
|
* Add a where clause comparing two values.
|
|
289
290
|
*/
|
|
290
|
-
where(
|
|
291
|
+
where<T extends Comparator>(
|
|
291
292
|
left: PropertyReferenceString<TContext> | LiteralValue,
|
|
292
|
-
operator:
|
|
293
|
-
right:
|
|
293
|
+
operator: T,
|
|
294
|
+
right: ComparatorValue<T, TContext>
|
|
294
295
|
): QueryBuilder<TContext>
|
|
295
296
|
|
|
296
297
|
/**
|
package/src/query/schema.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
InputReference,
|
|
4
4
|
PropertyReference,
|
|
5
5
|
PropertyReferenceString,
|
|
6
|
+
Schema,
|
|
6
7
|
WildcardReferenceString,
|
|
7
8
|
} from "./types.js"
|
|
8
9
|
import type { Collection } from "../collection"
|
|
@@ -30,6 +31,15 @@ export type LiteralValue =
|
|
|
30
31
|
| null
|
|
31
32
|
| undefined
|
|
32
33
|
|
|
34
|
+
// `in` and `not in` operators require an array of values
|
|
35
|
+
// the other operators require a single literal value
|
|
36
|
+
export type ComparatorValue<
|
|
37
|
+
T extends Comparator,
|
|
38
|
+
TContext extends Context<Schema>,
|
|
39
|
+
> = T extends `in` | `not in`
|
|
40
|
+
? Array<LiteralValue>
|
|
41
|
+
: PropertyReferenceString<TContext> | LiteralValue
|
|
42
|
+
|
|
33
43
|
// These versions are for use with methods on the query builder where we want to
|
|
34
44
|
// ensure that the argument is a string that does not start with "@".
|
|
35
45
|
// Can be combined with PropertyReference for validating references.
|
|
@@ -197,7 +207,7 @@ export type SelectCallback<TContext extends Context = Context> = (
|
|
|
197
207
|
context: TContext extends { schema: infer S } ? S : any
|
|
198
208
|
) => any
|
|
199
209
|
|
|
200
|
-
export type As<
|
|
210
|
+
export type As<_TContext extends Context = Context> = string
|
|
201
211
|
|
|
202
212
|
export type From<TContext extends Context = Context> = InputReference<{
|
|
203
213
|
baseSchema: TContext[`baseSchema`]
|
|
@@ -219,9 +229,9 @@ export type GroupBy<TContext extends Context = Context> =
|
|
|
219
229
|
| PropertyReference<TContext>
|
|
220
230
|
| Array<PropertyReference<TContext>>
|
|
221
231
|
|
|
222
|
-
export type Limit<
|
|
232
|
+
export type Limit<_TContext extends Context = Context> = number
|
|
223
233
|
|
|
224
|
-
export type Offset<
|
|
234
|
+
export type Offset<_TContext extends Context = Context> = number
|
|
225
235
|
|
|
226
236
|
export interface BaseQuery<TContext extends Context = Context> {
|
|
227
237
|
// The select clause is an array of either plain strings or objects mapping alias names
|
package/src/transactions.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createDeferred } from "./deferred"
|
|
2
2
|
import type { Deferred } from "./deferred"
|
|
3
3
|
import type {
|
|
4
|
+
MutationFn,
|
|
4
5
|
PendingMutation,
|
|
5
6
|
TransactionConfig,
|
|
6
7
|
TransactionState,
|
|
@@ -24,8 +25,8 @@ function generateUUID() {
|
|
|
24
25
|
})
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
const transactions: Array<Transaction
|
|
28
|
-
let transactionStack: Array<Transaction
|
|
28
|
+
const transactions: Array<Transaction<any>> = []
|
|
29
|
+
let transactionStack: Array<Transaction<any>> = []
|
|
29
30
|
|
|
30
31
|
export function createTransaction(config: TransactionConfig): Transaction {
|
|
31
32
|
if (typeof config.mutationFn === `undefined`) {
|
|
@@ -51,27 +52,27 @@ export function getActiveTransaction(): Transaction | undefined {
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
function registerTransaction(tx: Transaction) {
|
|
55
|
+
function registerTransaction(tx: Transaction<any>) {
|
|
55
56
|
transactionStack.push(tx)
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
function unregisterTransaction(tx: Transaction) {
|
|
59
|
+
function unregisterTransaction(tx: Transaction<any>) {
|
|
59
60
|
transactionStack = transactionStack.filter((t) => t.id !== tx.id)
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
function removeFromPendingList(tx: Transaction) {
|
|
63
|
+
function removeFromPendingList(tx: Transaction<any>) {
|
|
63
64
|
const index = transactions.findIndex((t) => t.id === tx.id)
|
|
64
65
|
if (index !== -1) {
|
|
65
66
|
transactions.splice(index, 1)
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
export class Transaction {
|
|
70
|
+
export class Transaction<T extends object = Record<string, unknown>> {
|
|
70
71
|
public id: string
|
|
71
72
|
public state: TransactionState
|
|
72
|
-
public mutationFn
|
|
73
|
-
public mutations: Array<PendingMutation<
|
|
74
|
-
public isPersisted: Deferred<Transaction
|
|
73
|
+
public mutationFn: MutationFn<T>
|
|
74
|
+
public mutations: Array<PendingMutation<T>>
|
|
75
|
+
public isPersisted: Deferred<Transaction<T>>
|
|
75
76
|
public autoCommit: boolean
|
|
76
77
|
public createdAt: Date
|
|
77
78
|
public metadata: Record<string, unknown>
|
|
@@ -80,12 +81,12 @@ export class Transaction {
|
|
|
80
81
|
error: Error
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
constructor(config: TransactionConfig) {
|
|
84
|
+
constructor(config: TransactionConfig<T>) {
|
|
84
85
|
this.id = config.id!
|
|
85
86
|
this.mutationFn = config.mutationFn
|
|
86
87
|
this.state = `pending`
|
|
87
88
|
this.mutations = []
|
|
88
|
-
this.isPersisted = createDeferred()
|
|
89
|
+
this.isPersisted = createDeferred<Transaction<T>>()
|
|
89
90
|
this.autoCommit = config.autoCommit ?? true
|
|
90
91
|
this.createdAt = new Date()
|
|
91
92
|
this.metadata = config.metadata ?? {}
|
|
@@ -99,7 +100,7 @@ export class Transaction {
|
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
mutate(callback: () => void): Transaction {
|
|
103
|
+
mutate(callback: () => void): Transaction<T> {
|
|
103
104
|
if (this.state !== `pending`) {
|
|
104
105
|
throw `You can no longer call .mutate() as the transaction is no longer pending`
|
|
105
106
|
}
|
|
@@ -121,7 +122,7 @@ export class Transaction {
|
|
|
121
122
|
applyMutations(mutations: Array<PendingMutation<any>>): void {
|
|
122
123
|
for (const newMutation of mutations) {
|
|
123
124
|
const existingIndex = this.mutations.findIndex(
|
|
124
|
-
(m) => m.
|
|
125
|
+
(m) => m.globalKey === newMutation.globalKey
|
|
125
126
|
)
|
|
126
127
|
|
|
127
128
|
if (existingIndex >= 0) {
|
|
@@ -134,7 +135,7 @@ export class Transaction {
|
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
rollback(config?: { isSecondaryRollback?: boolean }): Transaction {
|
|
138
|
+
rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {
|
|
138
139
|
const isSecondaryRollback = config?.isSecondaryRollback ?? false
|
|
139
140
|
if (this.state === `completed`) {
|
|
140
141
|
throw `You can no longer call .rollback() as the transaction is already completed`
|
|
@@ -146,10 +147,10 @@ export class Transaction {
|
|
|
146
147
|
// and roll them back as well.
|
|
147
148
|
if (!isSecondaryRollback) {
|
|
148
149
|
const mutationIds = new Set()
|
|
149
|
-
this.mutations.forEach((m) => mutationIds.add(m.
|
|
150
|
+
this.mutations.forEach((m) => mutationIds.add(m.globalKey))
|
|
150
151
|
for (const t of transactions) {
|
|
151
152
|
t.state === `pending` &&
|
|
152
|
-
t.mutations.some((m) => mutationIds.has(m.
|
|
153
|
+
t.mutations.some((m) => mutationIds.has(m.globalKey)) &&
|
|
153
154
|
t.rollback({ isSecondaryRollback: true })
|
|
154
155
|
}
|
|
155
156
|
}
|
|
@@ -166,14 +167,14 @@ export class Transaction {
|
|
|
166
167
|
const hasCalled = new Set()
|
|
167
168
|
for (const mutation of this.mutations) {
|
|
168
169
|
if (!hasCalled.has(mutation.collection.id)) {
|
|
169
|
-
mutation.collection.
|
|
170
|
+
mutation.collection.onTransactionStateChange()
|
|
170
171
|
mutation.collection.commitPendingTransactions()
|
|
171
172
|
hasCalled.add(mutation.collection.id)
|
|
172
173
|
}
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
176
|
|
|
176
|
-
async commit(): Promise<Transaction
|
|
177
|
+
async commit(): Promise<Transaction<T>> {
|
|
177
178
|
if (this.state !== `pending`) {
|
|
178
179
|
throw `You can no longer call .commit() as the transaction is no longer pending`
|
|
179
180
|
}
|
|
@@ -189,10 +190,11 @@ export class Transaction {
|
|
|
189
190
|
// Run mutationFn
|
|
190
191
|
try {
|
|
191
192
|
// At this point we know there's at least one mutation
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
// We've already verified mutations is non-empty, so this cast is safe
|
|
194
|
+
// Use a direct type assertion instead of object spreading to preserve the original type
|
|
195
|
+
await this.mutationFn({
|
|
196
|
+
transaction: this as unknown as TransactionWithMutations<T>,
|
|
197
|
+
})
|
|
196
198
|
|
|
197
199
|
this.setState(`completed`)
|
|
198
200
|
this.touchCollection()
|
package/src/types.ts
CHANGED
|
@@ -21,26 +21,34 @@ export type UtilsRecord = Record<string, Fn>
|
|
|
21
21
|
*/
|
|
22
22
|
export interface PendingMutation<T extends object = Record<string, unknown>> {
|
|
23
23
|
mutationId: string
|
|
24
|
-
original:
|
|
25
|
-
modified:
|
|
26
|
-
changes:
|
|
24
|
+
original: Partial<T>
|
|
25
|
+
modified: T
|
|
26
|
+
changes: Partial<T>
|
|
27
|
+
globalKey: string
|
|
27
28
|
key: any
|
|
28
29
|
type: OperationType
|
|
29
30
|
metadata: unknown
|
|
30
31
|
syncMetadata: Record<string, unknown>
|
|
31
32
|
createdAt: Date
|
|
32
33
|
updatedAt: Date
|
|
33
|
-
collection: Collection<T>
|
|
34
|
+
collection: Collection<T, any>
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* Configuration options for creating a new transaction
|
|
38
39
|
*/
|
|
39
|
-
export type MutationFnParams = {
|
|
40
|
-
transaction:
|
|
40
|
+
export type MutationFnParams<T extends object = Record<string, unknown>> = {
|
|
41
|
+
transaction: TransactionWithMutations<T>
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
export type MutationFn =
|
|
44
|
+
export type MutationFn<T extends object = Record<string, unknown>> = (
|
|
45
|
+
params: MutationFnParams<T>
|
|
46
|
+
) => Promise<any>
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Represents a non-empty array (at least one element)
|
|
50
|
+
*/
|
|
51
|
+
export type NonEmptyArray<T> = [T, ...Array<T>]
|
|
44
52
|
|
|
45
53
|
/**
|
|
46
54
|
* Utility type for a Transaction with at least one mutation
|
|
@@ -48,16 +56,16 @@ export type MutationFn = (params: MutationFnParams) => Promise<any>
|
|
|
48
56
|
*/
|
|
49
57
|
export type TransactionWithMutations<
|
|
50
58
|
T extends object = Record<string, unknown>,
|
|
51
|
-
> = Transaction & {
|
|
52
|
-
mutations:
|
|
59
|
+
> = Transaction<T> & {
|
|
60
|
+
mutations: NonEmptyArray<PendingMutation<T>>
|
|
53
61
|
}
|
|
54
62
|
|
|
55
|
-
export interface TransactionConfig {
|
|
63
|
+
export interface TransactionConfig<T extends object = Record<string, unknown>> {
|
|
56
64
|
/** Unique identifier for the transaction */
|
|
57
65
|
id?: string
|
|
58
66
|
/* If the transaction should autocommit after a mutate call or should commit be called explicitly */
|
|
59
67
|
autoCommit?: boolean
|
|
60
|
-
mutationFn: MutationFn
|
|
68
|
+
mutationFn: MutationFn<T>
|
|
61
69
|
/** Custom metadata to associate with the transaction */
|
|
62
70
|
metadata?: Record<string, unknown>
|
|
63
71
|
}
|
|
@@ -78,9 +86,12 @@ export type Row<TExtensions = never> = Record<string, Value<TExtensions>>
|
|
|
78
86
|
|
|
79
87
|
export type OperationType = `insert` | `update` | `delete`
|
|
80
88
|
|
|
81
|
-
export interface SyncConfig<
|
|
89
|
+
export interface SyncConfig<
|
|
90
|
+
T extends object = Record<string, unknown>,
|
|
91
|
+
TKey extends string | number = string | number,
|
|
92
|
+
> {
|
|
82
93
|
sync: (params: {
|
|
83
|
-
collection: Collection<T>
|
|
94
|
+
collection: Collection<T, TKey>
|
|
84
95
|
begin: () => void
|
|
85
96
|
write: (message: Omit<ChangeMessage<T>, `key`>) => void
|
|
86
97
|
commit: () => void
|
|
@@ -93,8 +104,11 @@ export interface SyncConfig<T extends object = Record<string, unknown>> {
|
|
|
93
104
|
getSyncMetadata?: () => Record<string, unknown>
|
|
94
105
|
}
|
|
95
106
|
|
|
96
|
-
export interface ChangeMessage<
|
|
97
|
-
|
|
107
|
+
export interface ChangeMessage<
|
|
108
|
+
T extends object = Record<string, unknown>,
|
|
109
|
+
TKey extends string | number = string | number,
|
|
110
|
+
> {
|
|
111
|
+
key: TKey
|
|
98
112
|
value: T
|
|
99
113
|
previousValue?: T
|
|
100
114
|
type: OperationType
|
|
@@ -134,11 +148,14 @@ export interface InsertConfig {
|
|
|
134
148
|
metadata?: Record<string, unknown>
|
|
135
149
|
}
|
|
136
150
|
|
|
137
|
-
export interface CollectionConfig<
|
|
151
|
+
export interface CollectionConfig<
|
|
152
|
+
T extends object = Record<string, unknown>,
|
|
153
|
+
TKey extends string | number = string | number,
|
|
154
|
+
> {
|
|
138
155
|
// If an id isn't passed in, a UUID will be
|
|
139
156
|
// generated for it.
|
|
140
157
|
id?: string
|
|
141
|
-
sync: SyncConfig<T>
|
|
158
|
+
sync: SyncConfig<T, TKey>
|
|
142
159
|
schema?: StandardSchema<T>
|
|
143
160
|
/**
|
|
144
161
|
* Function to extract the ID from an object
|
|
@@ -147,27 +164,27 @@ export interface CollectionConfig<T extends object = Record<string, unknown>> {
|
|
|
147
164
|
* @returns The ID string for the item
|
|
148
165
|
* @example
|
|
149
166
|
* // For a collection with a 'uuid' field as the primary key
|
|
150
|
-
*
|
|
167
|
+
* getKey: (item) => item.uuid
|
|
151
168
|
*/
|
|
152
|
-
|
|
169
|
+
getKey: (item: T) => TKey
|
|
153
170
|
/**
|
|
154
171
|
* Optional asynchronous handler function called before an insert operation
|
|
155
172
|
* @param params Object containing transaction and mutation information
|
|
156
173
|
* @returns Promise resolving to any value
|
|
157
174
|
*/
|
|
158
|
-
onInsert?: MutationFn
|
|
175
|
+
onInsert?: MutationFn<T>
|
|
159
176
|
/**
|
|
160
177
|
* Optional asynchronous handler function called before an update operation
|
|
161
178
|
* @param params Object containing transaction and mutation information
|
|
162
179
|
* @returns Promise resolving to any value
|
|
163
180
|
*/
|
|
164
|
-
onUpdate?: MutationFn
|
|
181
|
+
onUpdate?: MutationFn<T>
|
|
165
182
|
/**
|
|
166
183
|
* Optional asynchronous handler function called before a delete operation
|
|
167
184
|
* @param params Object containing transaction and mutation information
|
|
168
185
|
* @returns Promise resolving to any value
|
|
169
186
|
*/
|
|
170
|
-
onDelete?: MutationFn
|
|
187
|
+
onDelete?: MutationFn<T>
|
|
171
188
|
}
|
|
172
189
|
|
|
173
190
|
export type ChangesPayload<T extends object = Record<string, unknown>> = Array<
|
|
@@ -202,3 +219,8 @@ export type KeyedNamespacedRow = [unknown, NamespacedRow]
|
|
|
202
219
|
* a `select` clause.
|
|
203
220
|
*/
|
|
204
221
|
export type NamespacedAndKeyedStream = IStreamBuilder<KeyedNamespacedRow>
|
|
222
|
+
|
|
223
|
+
export type ChangeListener<
|
|
224
|
+
T extends object = Record<string, unknown>,
|
|
225
|
+
TKey extends string | number = string | number,
|
|
226
|
+
> = (changes: Array<ChangeMessage<T, TKey>>) => void
|