@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.
Files changed (43) hide show
  1. package/dist/cjs/collection.cjs +441 -284
  2. package/dist/cjs/collection.cjs.map +1 -1
  3. package/dist/cjs/collection.d.cts +103 -30
  4. package/dist/cjs/proxy.cjs +2 -2
  5. package/dist/cjs/proxy.cjs.map +1 -1
  6. package/dist/cjs/query/compiled-query.cjs +23 -37
  7. package/dist/cjs/query/compiled-query.cjs.map +1 -1
  8. package/dist/cjs/query/compiled-query.d.cts +2 -2
  9. package/dist/cjs/query/order-by.cjs +41 -38
  10. package/dist/cjs/query/order-by.cjs.map +1 -1
  11. package/dist/cjs/query/query-builder.cjs.map +1 -1
  12. package/dist/cjs/query/query-builder.d.cts +2 -2
  13. package/dist/cjs/query/schema.d.cts +5 -4
  14. package/dist/cjs/transactions.cjs +7 -6
  15. package/dist/cjs/transactions.cjs.map +1 -1
  16. package/dist/cjs/transactions.d.cts +9 -9
  17. package/dist/cjs/types.d.cts +28 -22
  18. package/dist/esm/collection.d.ts +103 -30
  19. package/dist/esm/collection.js +442 -285
  20. package/dist/esm/collection.js.map +1 -1
  21. package/dist/esm/proxy.js +2 -2
  22. package/dist/esm/proxy.js.map +1 -1
  23. package/dist/esm/query/compiled-query.d.ts +2 -2
  24. package/dist/esm/query/compiled-query.js +23 -37
  25. package/dist/esm/query/compiled-query.js.map +1 -1
  26. package/dist/esm/query/order-by.js +41 -38
  27. package/dist/esm/query/order-by.js.map +1 -1
  28. package/dist/esm/query/query-builder.d.ts +2 -2
  29. package/dist/esm/query/query-builder.js.map +1 -1
  30. package/dist/esm/query/schema.d.ts +5 -4
  31. package/dist/esm/transactions.d.ts +9 -9
  32. package/dist/esm/transactions.js +7 -6
  33. package/dist/esm/transactions.js.map +1 -1
  34. package/dist/esm/types.d.ts +28 -22
  35. package/package.json +2 -2
  36. package/src/collection.ts +624 -372
  37. package/src/proxy.ts +2 -2
  38. package/src/query/compiled-query.ts +26 -37
  39. package/src/query/order-by.ts +69 -67
  40. package/src/query/query-builder.ts +4 -3
  41. package/src/query/schema.ts +13 -3
  42. package/src/transactions.ts +24 -22
  43. 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(sobj, prop, value) {
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(ptarget, prop, descriptor) {
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 unsubscribeEffect?: () => void
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
- getId: (val: unknown) => {
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
- getId: (item: ChangeMessage[`value`]) => any
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 = getId(change.value)
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
- batch(() => {
165
- Object.entries(this.inputCollections).forEach(([key, collection]) => {
166
- this.sendChangesToInput(
167
- key,
168
- collection.currentStateAsChanges(),
169
- collection.config.getId
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
- const changeEffect = new Effect({
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.unsubscribeEffect?.()
206
- this.unsubscribeEffect = undefined
194
+ this.unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe())
195
+ this.unsubscribeCallbacks = []
207
196
  this.state = `stopped`
208
197
  }
209
198
  }
@@ -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: Array<{
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
- const val = evaluateOperandOnNamespacedRow(
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 comparator = (a: unknown, b: unknown): number => {
127
- // if a and b are both numbers compare them directly
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
- let result: number
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
- // if a and b are both null/undefined, return 0
180
- if (a == null && b == null) {
181
- return 0
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: Comparator,
293
- right: PropertyReferenceString<TContext> | LiteralValue
293
+ operator: T,
294
+ right: ComparatorValue<T, TContext>
294
295
  ): QueryBuilder<TContext>
295
296
 
296
297
  /**
@@ -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<TContext extends Context = Context> = string
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<TContext extends Context = Context> = number
232
+ export type Limit<_TContext extends Context = Context> = number
223
233
 
224
- export type Offset<TContext extends Context = Context> = number
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
@@ -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<any>>
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.key === newMutation.key
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.key))
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.key)) &&
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.transactions.setState((state) => state)
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
- // Use type assertion to tell TypeScript about this guarantee
193
- const transactionWithMutations =
194
- this as unknown as TransactionWithMutations
195
- await this.mutationFn({ transaction: transactionWithMutations })
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: Record<string, unknown>
25
- modified: Record<string, unknown>
26
- changes: Record<string, unknown>
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: Transaction
40
+ export type MutationFnParams<T extends object = Record<string, unknown>> = {
41
+ transaction: TransactionWithMutations<T>
41
42
  }
42
43
 
43
- export type MutationFn = (params: MutationFnParams) => Promise<any>
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: [PendingMutation<T>, ...Array<PendingMutation<T>>]
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<T extends object = Record<string, unknown>> {
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<T extends object = Record<string, unknown>> {
97
- key: any
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<T extends object = Record<string, unknown>> {
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
- * getId: (item) => item.uuid
167
+ * getKey: (item) => item.uuid
151
168
  */
152
- getId: (item: T) => any
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