@tanstack/db 0.0.6 → 0.0.8
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 +452 -286
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +115 -26
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/index.d.cts +1 -1
- package/dist/cjs/proxy.cjs +2 -2
- package/dist/cjs/proxy.cjs.map +1 -1
- package/dist/cjs/query/compiled-query.cjs +24 -38
- 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/schema.d.cts +3 -3
- 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 +36 -22
- package/dist/esm/collection.d.ts +115 -26
- package/dist/esm/collection.js +453 -287
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/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 +25 -39
- 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/schema.d.ts +3 -3
- 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 +36 -22
- package/package.json +2 -2
- package/src/collection.ts +652 -368
- package/src/index.ts +1 -1
- package/src/proxy.ts +2 -2
- package/src/query/compiled-query.ts +29 -39
- package/src/query/order-by.ts +69 -67
- package/src/query/schema.ts +3 -3
- package/src/transactions.ts +24 -22
- package/src/types.ts +54 -22
package/src/index.ts
CHANGED
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,7 +1,7 @@
|
|
|
1
1
|
import { D2, MessageType, MultiSet, output } from "@electric-sql/d2ts"
|
|
2
|
-
import {
|
|
3
|
-
import { Collection } from "../collection.js"
|
|
2
|
+
import { createCollection } from "../collection.js"
|
|
4
3
|
import { compileQueryPipeline } from "./pipeline-compiler.js"
|
|
4
|
+
import type { Collection } from "../collection.js"
|
|
5
5
|
import type { ChangeMessage, SyncConfig } from "../types.js"
|
|
6
6
|
import type {
|
|
7
7
|
IStreamBuilder,
|
|
@@ -26,7 +26,7 @@ export class CompiledQuery<TResults extends object = Record<string, unknown>> {
|
|
|
26
26
|
private resultCollection: Collection<TResults>
|
|
27
27
|
public state: `compiled` | `running` | `stopped` = `compiled`
|
|
28
28
|
private version = 0
|
|
29
|
-
private
|
|
29
|
+
private unsubscribeCallbacks: Array<() => void> = []
|
|
30
30
|
|
|
31
31
|
constructor(queryBuilder: QueryBuilder<Context<Schema>>) {
|
|
32
32
|
const query = queryBuilder._query
|
|
@@ -97,9 +97,9 @@ export class CompiledQuery<TResults extends object = Record<string, unknown>> {
|
|
|
97
97
|
|
|
98
98
|
this.graph = graph
|
|
99
99
|
this.inputs = inputs
|
|
100
|
-
this.resultCollection =
|
|
100
|
+
this.resultCollection = createCollection<TResults>({
|
|
101
101
|
id: crypto.randomUUID(), // TODO: remove when we don't require any more
|
|
102
|
-
|
|
102
|
+
getKey: (val: unknown) => {
|
|
103
103
|
return (val as any)._key
|
|
104
104
|
},
|
|
105
105
|
sync: {
|
|
@@ -115,12 +115,12 @@ export class CompiledQuery<TResults extends object = Record<string, unknown>> {
|
|
|
115
115
|
private sendChangesToInput(
|
|
116
116
|
inputKey: string,
|
|
117
117
|
changes: Array<ChangeMessage>,
|
|
118
|
-
|
|
118
|
+
getKey: (item: ChangeMessage[`value`]) => any
|
|
119
119
|
) {
|
|
120
120
|
const input = this.inputs[inputKey]!
|
|
121
121
|
const multiSetArray: MultiSetArray<unknown> = []
|
|
122
122
|
for (const change of changes) {
|
|
123
|
-
const key =
|
|
123
|
+
const key = getKey(change.value)
|
|
124
124
|
if (change.type === `insert`) {
|
|
125
125
|
multiSetArray.push([[key, change.value], 1])
|
|
126
126
|
} else if (change.type === `update`) {
|
|
@@ -160,39 +160,29 @@ export class CompiledQuery<TResults extends object = Record<string, unknown>> {
|
|
|
160
160
|
throw new Error(`Query is stopped`)
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
})
|
|
171
|
-
this.incrementVersion()
|
|
172
|
-
this.sendFrontierToAllInputs()
|
|
173
|
-
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
|
+
)
|
|
174
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
|
+
})
|
|
175
183
|
|
|
176
|
-
|
|
177
|
-
fn: () => {
|
|
178
|
-
batch(() => {
|
|
179
|
-
Object.entries(this.inputCollections).forEach(([key, collection]) => {
|
|
180
|
-
this.sendChangesToInput(
|
|
181
|
-
key,
|
|
182
|
-
collection.derivedChanges.state,
|
|
183
|
-
collection.config.getId
|
|
184
|
-
)
|
|
185
|
-
})
|
|
186
|
-
this.incrementVersion()
|
|
187
|
-
this.sendFrontierToAllInputs()
|
|
188
|
-
this.runGraph()
|
|
189
|
-
})
|
|
190
|
-
},
|
|
191
|
-
deps: Object.values(this.inputCollections).map(
|
|
192
|
-
(collection) => collection.derivedChanges
|
|
193
|
-
),
|
|
184
|
+
this.unsubscribeCallbacks.push(unsubscribe)
|
|
194
185
|
})
|
|
195
|
-
this.unsubscribeEffect = changeEffect.mount()
|
|
196
186
|
|
|
197
187
|
this.state = `running`
|
|
198
188
|
return () => {
|
|
@@ -201,8 +191,8 @@ export class CompiledQuery<TResults extends object = Record<string, unknown>> {
|
|
|
201
191
|
}
|
|
202
192
|
|
|
203
193
|
stop() {
|
|
204
|
-
this.
|
|
205
|
-
this.
|
|
194
|
+
this.unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe())
|
|
195
|
+
this.unsubscribeCallbacks = []
|
|
206
196
|
this.state = `stopped`
|
|
207
197
|
}
|
|
208
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) {
|
package/src/query/schema.ts
CHANGED
|
@@ -197,7 +197,7 @@ export type SelectCallback<TContext extends Context = Context> = (
|
|
|
197
197
|
context: TContext extends { schema: infer S } ? S : any
|
|
198
198
|
) => any
|
|
199
199
|
|
|
200
|
-
export type As<
|
|
200
|
+
export type As<_TContext extends Context = Context> = string
|
|
201
201
|
|
|
202
202
|
export type From<TContext extends Context = Context> = InputReference<{
|
|
203
203
|
baseSchema: TContext[`baseSchema`]
|
|
@@ -219,9 +219,9 @@ export type GroupBy<TContext extends Context = Context> =
|
|
|
219
219
|
| PropertyReference<TContext>
|
|
220
220
|
| Array<PropertyReference<TContext>>
|
|
221
221
|
|
|
222
|
-
export type Limit<
|
|
222
|
+
export type Limit<_TContext extends Context = Context> = number
|
|
223
223
|
|
|
224
|
-
export type Offset<
|
|
224
|
+
export type Offset<_TContext extends Context = Context> = number
|
|
225
225
|
|
|
226
226
|
export interface BaseQuery<TContext extends Context = Context> {
|
|
227
227
|
// 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
|
@@ -5,32 +5,50 @@ import type { Transaction } from "./transactions"
|
|
|
5
5
|
|
|
6
6
|
export type TransactionState = `pending` | `persisting` | `completed` | `failed`
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Represents a utility function that can be attached to a collection
|
|
10
|
+
*/
|
|
11
|
+
export type Fn = (...args: Array<any>) => any
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A record of utility functions that can be attached to a collection
|
|
15
|
+
*/
|
|
16
|
+
export type UtilsRecord = Record<string, Fn>
|
|
17
|
+
|
|
8
18
|
/**
|
|
9
19
|
* Represents a pending mutation within a transaction
|
|
10
20
|
* Contains information about the original and modified data, as well as metadata
|
|
11
21
|
*/
|
|
12
22
|
export interface PendingMutation<T extends object = Record<string, unknown>> {
|
|
13
23
|
mutationId: string
|
|
14
|
-
original:
|
|
15
|
-
modified:
|
|
16
|
-
changes:
|
|
24
|
+
original: Partial<T>
|
|
25
|
+
modified: T
|
|
26
|
+
changes: Partial<T>
|
|
27
|
+
globalKey: string
|
|
17
28
|
key: any
|
|
18
29
|
type: OperationType
|
|
19
30
|
metadata: unknown
|
|
20
31
|
syncMetadata: Record<string, unknown>
|
|
21
32
|
createdAt: Date
|
|
22
33
|
updatedAt: Date
|
|
23
|
-
collection: Collection<T>
|
|
34
|
+
collection: Collection<T, any>
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
/**
|
|
27
38
|
* Configuration options for creating a new transaction
|
|
28
39
|
*/
|
|
29
|
-
export type MutationFnParams = {
|
|
30
|
-
transaction:
|
|
40
|
+
export type MutationFnParams<T extends object = Record<string, unknown>> = {
|
|
41
|
+
transaction: TransactionWithMutations<T>
|
|
31
42
|
}
|
|
32
43
|
|
|
33
|
-
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>]
|
|
34
52
|
|
|
35
53
|
/**
|
|
36
54
|
* Utility type for a Transaction with at least one mutation
|
|
@@ -38,16 +56,16 @@ export type MutationFn = (params: MutationFnParams) => Promise<any>
|
|
|
38
56
|
*/
|
|
39
57
|
export type TransactionWithMutations<
|
|
40
58
|
T extends object = Record<string, unknown>,
|
|
41
|
-
> = Transaction & {
|
|
42
|
-
mutations:
|
|
59
|
+
> = Transaction<T> & {
|
|
60
|
+
mutations: NonEmptyArray<PendingMutation<T>>
|
|
43
61
|
}
|
|
44
62
|
|
|
45
|
-
export interface TransactionConfig {
|
|
63
|
+
export interface TransactionConfig<T extends object = Record<string, unknown>> {
|
|
46
64
|
/** Unique identifier for the transaction */
|
|
47
65
|
id?: string
|
|
48
66
|
/* If the transaction should autocommit after a mutate call or should commit be called explicitly */
|
|
49
67
|
autoCommit?: boolean
|
|
50
|
-
mutationFn: MutationFn
|
|
68
|
+
mutationFn: MutationFn<T>
|
|
51
69
|
/** Custom metadata to associate with the transaction */
|
|
52
70
|
metadata?: Record<string, unknown>
|
|
53
71
|
}
|
|
@@ -68,9 +86,12 @@ export type Row<TExtensions = never> = Record<string, Value<TExtensions>>
|
|
|
68
86
|
|
|
69
87
|
export type OperationType = `insert` | `update` | `delete`
|
|
70
88
|
|
|
71
|
-
export interface SyncConfig<
|
|
89
|
+
export interface SyncConfig<
|
|
90
|
+
T extends object = Record<string, unknown>,
|
|
91
|
+
TKey extends string | number = string | number,
|
|
92
|
+
> {
|
|
72
93
|
sync: (params: {
|
|
73
|
-
collection: Collection<T>
|
|
94
|
+
collection: Collection<T, TKey>
|
|
74
95
|
begin: () => void
|
|
75
96
|
write: (message: Omit<ChangeMessage<T>, `key`>) => void
|
|
76
97
|
commit: () => void
|
|
@@ -83,8 +104,11 @@ export interface SyncConfig<T extends object = Record<string, unknown>> {
|
|
|
83
104
|
getSyncMetadata?: () => Record<string, unknown>
|
|
84
105
|
}
|
|
85
106
|
|
|
86
|
-
export interface ChangeMessage<
|
|
87
|
-
|
|
107
|
+
export interface ChangeMessage<
|
|
108
|
+
T extends object = Record<string, unknown>,
|
|
109
|
+
TKey extends string | number = string | number,
|
|
110
|
+
> {
|
|
111
|
+
key: TKey
|
|
88
112
|
value: T
|
|
89
113
|
previousValue?: T
|
|
90
114
|
type: OperationType
|
|
@@ -124,11 +148,14 @@ export interface InsertConfig {
|
|
|
124
148
|
metadata?: Record<string, unknown>
|
|
125
149
|
}
|
|
126
150
|
|
|
127
|
-
export interface CollectionConfig<
|
|
151
|
+
export interface CollectionConfig<
|
|
152
|
+
T extends object = Record<string, unknown>,
|
|
153
|
+
TKey extends string | number = string | number,
|
|
154
|
+
> {
|
|
128
155
|
// If an id isn't passed in, a UUID will be
|
|
129
156
|
// generated for it.
|
|
130
157
|
id?: string
|
|
131
|
-
sync: SyncConfig<T>
|
|
158
|
+
sync: SyncConfig<T, TKey>
|
|
132
159
|
schema?: StandardSchema<T>
|
|
133
160
|
/**
|
|
134
161
|
* Function to extract the ID from an object
|
|
@@ -137,27 +164,27 @@ export interface CollectionConfig<T extends object = Record<string, unknown>> {
|
|
|
137
164
|
* @returns The ID string for the item
|
|
138
165
|
* @example
|
|
139
166
|
* // For a collection with a 'uuid' field as the primary key
|
|
140
|
-
*
|
|
167
|
+
* getKey: (item) => item.uuid
|
|
141
168
|
*/
|
|
142
|
-
|
|
169
|
+
getKey: (item: T) => TKey
|
|
143
170
|
/**
|
|
144
171
|
* Optional asynchronous handler function called before an insert operation
|
|
145
172
|
* @param params Object containing transaction and mutation information
|
|
146
173
|
* @returns Promise resolving to any value
|
|
147
174
|
*/
|
|
148
|
-
onInsert?: MutationFn
|
|
175
|
+
onInsert?: MutationFn<T>
|
|
149
176
|
/**
|
|
150
177
|
* Optional asynchronous handler function called before an update operation
|
|
151
178
|
* @param params Object containing transaction and mutation information
|
|
152
179
|
* @returns Promise resolving to any value
|
|
153
180
|
*/
|
|
154
|
-
onUpdate?: MutationFn
|
|
181
|
+
onUpdate?: MutationFn<T>
|
|
155
182
|
/**
|
|
156
183
|
* Optional asynchronous handler function called before a delete operation
|
|
157
184
|
* @param params Object containing transaction and mutation information
|
|
158
185
|
* @returns Promise resolving to any value
|
|
159
186
|
*/
|
|
160
|
-
onDelete?: MutationFn
|
|
187
|
+
onDelete?: MutationFn<T>
|
|
161
188
|
}
|
|
162
189
|
|
|
163
190
|
export type ChangesPayload<T extends object = Record<string, unknown>> = Array<
|
|
@@ -192,3 +219,8 @@ export type KeyedNamespacedRow = [unknown, NamespacedRow]
|
|
|
192
219
|
* a `select` clause.
|
|
193
220
|
*/
|
|
194
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
|