@tanstack/db 0.5.12 → 0.5.14
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/changes.cjs +2 -0
- package/dist/cjs/collection/changes.cjs.map +1 -1
- package/dist/cjs/collection/subscription.cjs +120 -6
- package/dist/cjs/collection/subscription.cjs.map +1 -1
- package/dist/cjs/collection/subscription.d.cts +28 -0
- package/dist/cjs/collection/sync.cjs +11 -6
- package/dist/cjs/collection/sync.cjs.map +1 -1
- package/dist/cjs/types.d.cts +10 -3
- package/dist/esm/collection/changes.js +2 -0
- package/dist/esm/collection/changes.js.map +1 -1
- package/dist/esm/collection/subscription.d.ts +28 -0
- package/dist/esm/collection/subscription.js +120 -6
- package/dist/esm/collection/subscription.js.map +1 -1
- package/dist/esm/collection/sync.js +11 -6
- package/dist/esm/collection/sync.js.map +1 -1
- package/dist/esm/types.d.ts +10 -3
- package/package.json +1 -1
- package/src/collection/changes.ts +4 -0
- package/src/collection/subscription.ts +216 -10
- package/src/collection/sync.ts +21 -9
- package/src/types.ts +25 -6
|
@@ -52,6 +52,11 @@ export class CollectionSubscription
|
|
|
52
52
|
{
|
|
53
53
|
private loadedInitialState = false
|
|
54
54
|
|
|
55
|
+
// Flag to skip filtering in filterAndFlipChanges.
|
|
56
|
+
// This is separate from loadedInitialState because we want to allow
|
|
57
|
+
// requestSnapshot to still work even when filtering is skipped.
|
|
58
|
+
private skipFiltering = false
|
|
59
|
+
|
|
55
60
|
// Flag to indicate that we have sent at least 1 snapshot.
|
|
56
61
|
// While `snapshotSent` is false we filter out all changes from subscription to the collection.
|
|
57
62
|
private snapshotSent = false
|
|
@@ -79,6 +84,16 @@ export class CollectionSubscription
|
|
|
79
84
|
private _status: SubscriptionStatus = `ready`
|
|
80
85
|
private pendingLoadSubsetPromises: Set<Promise<void>> = new Set()
|
|
81
86
|
|
|
87
|
+
// Cleanup function for truncate event listener
|
|
88
|
+
private truncateCleanup: (() => void) | undefined
|
|
89
|
+
|
|
90
|
+
// Truncate buffering state
|
|
91
|
+
// When a truncate occurs, we buffer changes until all loadSubset refetches complete
|
|
92
|
+
// This prevents a flash of missing content between deletes and new inserts
|
|
93
|
+
private isBufferingForTruncate = false
|
|
94
|
+
private truncateBuffer: Array<Array<ChangeMessage<any, any>>> = []
|
|
95
|
+
private pendingTruncateRefetches: Set<Promise<void>> = new Set()
|
|
96
|
+
|
|
82
97
|
public get status(): SubscriptionStatus {
|
|
83
98
|
return this._status
|
|
84
99
|
}
|
|
@@ -111,6 +126,123 @@ export class CollectionSubscription
|
|
|
111
126
|
this.filteredCallback = options.whereExpression
|
|
112
127
|
? createFilteredCallback(this.callback, options)
|
|
113
128
|
: this.callback
|
|
129
|
+
|
|
130
|
+
// Listen for truncate events to re-request data after must-refetch
|
|
131
|
+
// When a truncate happens (e.g., from a 409 must-refetch), all collection data is cleared.
|
|
132
|
+
// We need to re-request all previously loaded subsets to repopulate the data.
|
|
133
|
+
this.truncateCleanup = this.collection.on(`truncate`, () => {
|
|
134
|
+
this.handleTruncate()
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Handle collection truncate event by resetting state and re-requesting subsets.
|
|
140
|
+
* This is called when the sync layer receives a must-refetch and clears all data.
|
|
141
|
+
*
|
|
142
|
+
* To prevent a flash of missing content, we buffer all changes (deletes from truncate
|
|
143
|
+
* and inserts from refetch) until all loadSubset promises resolve, then emit them together.
|
|
144
|
+
*/
|
|
145
|
+
private handleTruncate() {
|
|
146
|
+
// Copy the loaded subsets before clearing (we'll re-request them)
|
|
147
|
+
const subsetsToReload = [...this.loadedSubsets]
|
|
148
|
+
|
|
149
|
+
// Only buffer if there's an actual loadSubset handler that can do async work.
|
|
150
|
+
// Without a loadSubset handler, there's nothing to re-request and no reason to buffer.
|
|
151
|
+
// This prevents unnecessary buffering in eager sync mode or when loadSubset isn't implemented.
|
|
152
|
+
const hasLoadSubsetHandler = this.collection._sync.syncLoadSubsetFn !== null
|
|
153
|
+
|
|
154
|
+
// If there are no subsets to reload OR no loadSubset handler, just reset state
|
|
155
|
+
if (subsetsToReload.length === 0 || !hasLoadSubsetHandler) {
|
|
156
|
+
this.snapshotSent = false
|
|
157
|
+
this.loadedInitialState = false
|
|
158
|
+
this.limitedSnapshotRowCount = 0
|
|
159
|
+
this.lastSentKey = undefined
|
|
160
|
+
this.loadedSubsets = []
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Start buffering BEFORE we receive the delete events from the truncate commit
|
|
165
|
+
// This ensures we capture both the deletes and subsequent inserts
|
|
166
|
+
this.isBufferingForTruncate = true
|
|
167
|
+
this.truncateBuffer = []
|
|
168
|
+
this.pendingTruncateRefetches.clear()
|
|
169
|
+
|
|
170
|
+
// Reset snapshot/pagination tracking state
|
|
171
|
+
// Note: We don't need to populate sentKeys here because filterAndFlipChanges
|
|
172
|
+
// will skip the delete filter when isBufferingForTruncate is true
|
|
173
|
+
this.snapshotSent = false
|
|
174
|
+
this.loadedInitialState = false
|
|
175
|
+
this.limitedSnapshotRowCount = 0
|
|
176
|
+
this.lastSentKey = undefined
|
|
177
|
+
|
|
178
|
+
// Clear the loadedSubsets array since we're re-requesting fresh
|
|
179
|
+
this.loadedSubsets = []
|
|
180
|
+
|
|
181
|
+
// Defer the loadSubset calls to a microtask so the truncate commit's delete events
|
|
182
|
+
// are buffered BEFORE the loadSubset calls potentially trigger nested commits.
|
|
183
|
+
// This ensures correct event ordering: deletes first, then inserts.
|
|
184
|
+
queueMicrotask(() => {
|
|
185
|
+
// Check if we were unsubscribed while waiting
|
|
186
|
+
if (!this.isBufferingForTruncate) {
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Re-request all previously loaded subsets and track their promises
|
|
191
|
+
for (const options of subsetsToReload) {
|
|
192
|
+
const syncResult = this.collection._sync.loadSubset(options)
|
|
193
|
+
|
|
194
|
+
// Track this loadSubset call so we can unload it later
|
|
195
|
+
this.loadedSubsets.push(options)
|
|
196
|
+
this.trackLoadSubsetPromise(syncResult)
|
|
197
|
+
|
|
198
|
+
// Track the promise for buffer flushing
|
|
199
|
+
if (syncResult instanceof Promise) {
|
|
200
|
+
this.pendingTruncateRefetches.add(syncResult)
|
|
201
|
+
syncResult
|
|
202
|
+
.catch(() => {
|
|
203
|
+
// Ignore errors - we still want to flush the buffer even if some requests fail
|
|
204
|
+
})
|
|
205
|
+
.finally(() => {
|
|
206
|
+
this.pendingTruncateRefetches.delete(syncResult)
|
|
207
|
+
this.checkTruncateRefetchComplete()
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// If all loadSubset calls were synchronous (returned true), flush now
|
|
213
|
+
// At this point, delete events have already been buffered from the truncate commit
|
|
214
|
+
if (this.pendingTruncateRefetches.size === 0) {
|
|
215
|
+
this.flushTruncateBuffer()
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Check if all truncate refetch promises have completed and flush buffer if so
|
|
222
|
+
*/
|
|
223
|
+
private checkTruncateRefetchComplete() {
|
|
224
|
+
if (
|
|
225
|
+
this.pendingTruncateRefetches.size === 0 &&
|
|
226
|
+
this.isBufferingForTruncate
|
|
227
|
+
) {
|
|
228
|
+
this.flushTruncateBuffer()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Flush the truncate buffer, emitting all buffered changes to the callback
|
|
234
|
+
*/
|
|
235
|
+
private flushTruncateBuffer() {
|
|
236
|
+
this.isBufferingForTruncate = false
|
|
237
|
+
|
|
238
|
+
// Flatten all buffered changes into a single array for atomic emission
|
|
239
|
+
// This ensures consumers see all truncate changes (deletes + inserts) in one callback
|
|
240
|
+
const merged = this.truncateBuffer.flat()
|
|
241
|
+
if (merged.length > 0) {
|
|
242
|
+
this.filteredCallback(merged)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.truncateBuffer = []
|
|
114
246
|
}
|
|
115
247
|
|
|
116
248
|
setOrderByIndex(index: IndexInterface<any>) {
|
|
@@ -174,7 +306,16 @@ export class CollectionSubscription
|
|
|
174
306
|
|
|
175
307
|
emitEvents(changes: Array<ChangeMessage<any, any>>) {
|
|
176
308
|
const newChanges = this.filterAndFlipChanges(changes)
|
|
177
|
-
|
|
309
|
+
|
|
310
|
+
if (this.isBufferingForTruncate) {
|
|
311
|
+
// Buffer the changes instead of emitting immediately
|
|
312
|
+
// This prevents a flash of missing content during truncate/refetch
|
|
313
|
+
if (newChanges.length > 0) {
|
|
314
|
+
this.truncateBuffer.push(newChanges)
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
this.filteredCallback(newChanges)
|
|
318
|
+
}
|
|
178
319
|
}
|
|
179
320
|
|
|
180
321
|
/**
|
|
@@ -244,6 +385,13 @@ export class CollectionSubscription
|
|
|
244
385
|
(change) => !this.sentKeys.has(change.key),
|
|
245
386
|
)
|
|
246
387
|
|
|
388
|
+
// Add keys to sentKeys BEFORE calling callback to prevent race condition.
|
|
389
|
+
// If a change event arrives while the callback is executing, it will see
|
|
390
|
+
// the keys already in sentKeys and filter out duplicates correctly.
|
|
391
|
+
for (const change of filteredSnapshot) {
|
|
392
|
+
this.sentKeys.add(change.key)
|
|
393
|
+
}
|
|
394
|
+
|
|
247
395
|
this.snapshotSent = true
|
|
248
396
|
this.callback(filteredSnapshot)
|
|
249
397
|
return true
|
|
@@ -367,6 +515,13 @@ export class CollectionSubscription
|
|
|
367
515
|
// Use the current count as the offset for this load
|
|
368
516
|
const currentOffset = this.limitedSnapshotRowCount
|
|
369
517
|
|
|
518
|
+
// Add keys to sentKeys BEFORE calling callback to prevent race condition.
|
|
519
|
+
// If a change event arrives while the callback is executing, it will see
|
|
520
|
+
// the keys already in sentKeys and filter out duplicates correctly.
|
|
521
|
+
for (const change of changes) {
|
|
522
|
+
this.sentKeys.add(change.key)
|
|
523
|
+
}
|
|
524
|
+
|
|
370
525
|
this.callback(changes)
|
|
371
526
|
|
|
372
527
|
// Update the row count and last key after sending (for next call's offset/cursor)
|
|
@@ -441,25 +596,52 @@ export class CollectionSubscription
|
|
|
441
596
|
* Filters and flips changes for keys that have not been sent yet.
|
|
442
597
|
* Deletes are filtered out for keys that have not been sent yet.
|
|
443
598
|
* Updates are flipped into inserts for keys that have not been sent yet.
|
|
599
|
+
* Duplicate inserts are filtered out to prevent D2 multiplicity > 1.
|
|
444
600
|
*/
|
|
445
601
|
private filterAndFlipChanges(changes: Array<ChangeMessage<any, any>>) {
|
|
446
|
-
if (this.loadedInitialState) {
|
|
447
|
-
// We loaded the entire initial state
|
|
602
|
+
if (this.loadedInitialState || this.skipFiltering) {
|
|
603
|
+
// We loaded the entire initial state or filtering is explicitly skipped
|
|
448
604
|
// so no need to filter or flip changes
|
|
449
605
|
return changes
|
|
450
606
|
}
|
|
451
607
|
|
|
608
|
+
// When buffering for truncate, we need all changes (including deletes) to pass through.
|
|
609
|
+
// This is important because:
|
|
610
|
+
// 1. If loadedInitialState was previously true, sentKeys will be empty
|
|
611
|
+
// (trackSentKeys early-returns when loadedInitialState is true)
|
|
612
|
+
// 2. The truncate deletes are for keys that WERE sent to the subscriber
|
|
613
|
+
// 3. We're collecting all changes atomically, so filtering doesn't make sense
|
|
614
|
+
const skipDeleteFilter = this.isBufferingForTruncate
|
|
615
|
+
|
|
452
616
|
const newChanges = []
|
|
453
617
|
for (const change of changes) {
|
|
454
618
|
let newChange = change
|
|
455
|
-
|
|
619
|
+
const keyInSentKeys = this.sentKeys.has(change.key)
|
|
620
|
+
|
|
621
|
+
if (!keyInSentKeys) {
|
|
456
622
|
if (change.type === `update`) {
|
|
457
623
|
newChange = { ...change, type: `insert`, previousValue: undefined }
|
|
458
624
|
} else if (change.type === `delete`) {
|
|
459
|
-
//
|
|
460
|
-
|
|
625
|
+
// Filter out deletes for keys that have not been sent,
|
|
626
|
+
// UNLESS we're buffering for truncate (where all deletes should pass through)
|
|
627
|
+
if (!skipDeleteFilter) {
|
|
628
|
+
continue
|
|
629
|
+
}
|
|
461
630
|
}
|
|
462
631
|
this.sentKeys.add(change.key)
|
|
632
|
+
} else {
|
|
633
|
+
// Key was already sent - handle based on change type
|
|
634
|
+
if (change.type === `insert`) {
|
|
635
|
+
// Filter out duplicate inserts - the key was already inserted.
|
|
636
|
+
// This prevents D2 multiplicity from going above 1, which would
|
|
637
|
+
// cause deletes to not properly remove items (multiplicity would
|
|
638
|
+
// go from 2 to 1 instead of 1 to 0).
|
|
639
|
+
continue
|
|
640
|
+
} else if (change.type === `delete`) {
|
|
641
|
+
// Remove from sentKeys so future inserts for this key are allowed
|
|
642
|
+
// (e.g., after truncate + reinsert)
|
|
643
|
+
this.sentKeys.delete(change.key)
|
|
644
|
+
}
|
|
463
645
|
}
|
|
464
646
|
newChanges.push(newChange)
|
|
465
647
|
}
|
|
@@ -467,18 +649,42 @@ export class CollectionSubscription
|
|
|
467
649
|
}
|
|
468
650
|
|
|
469
651
|
private trackSentKeys(changes: Array<ChangeMessage<any, string | number>>) {
|
|
470
|
-
if (this.loadedInitialState) {
|
|
471
|
-
// No need to track sent keys if we loaded the entire state.
|
|
472
|
-
// Since
|
|
652
|
+
if (this.loadedInitialState || this.skipFiltering) {
|
|
653
|
+
// No need to track sent keys if we loaded the entire state or filtering is skipped.
|
|
654
|
+
// Since filtering won't be applied, all keys are effectively "observed".
|
|
473
655
|
return
|
|
474
656
|
}
|
|
475
657
|
|
|
476
658
|
for (const change of changes) {
|
|
477
|
-
|
|
659
|
+
if (change.type === `delete`) {
|
|
660
|
+
// Remove deleted keys from sentKeys so future re-inserts are allowed
|
|
661
|
+
this.sentKeys.delete(change.key)
|
|
662
|
+
} else {
|
|
663
|
+
// For inserts and updates, track the key as sent
|
|
664
|
+
this.sentKeys.add(change.key)
|
|
665
|
+
}
|
|
478
666
|
}
|
|
479
667
|
}
|
|
480
668
|
|
|
669
|
+
/**
|
|
670
|
+
* Mark that the subscription should not filter any changes.
|
|
671
|
+
* This is used when includeInitialState is explicitly set to false,
|
|
672
|
+
* meaning the caller doesn't want initial state but does want ALL future changes.
|
|
673
|
+
*/
|
|
674
|
+
markAllStateAsSeen() {
|
|
675
|
+
this.skipFiltering = true
|
|
676
|
+
}
|
|
677
|
+
|
|
481
678
|
unsubscribe() {
|
|
679
|
+
// Clean up truncate event listener
|
|
680
|
+
this.truncateCleanup?.()
|
|
681
|
+
this.truncateCleanup = undefined
|
|
682
|
+
|
|
683
|
+
// Clean up truncate buffer state
|
|
684
|
+
this.isBufferingForTruncate = false
|
|
685
|
+
this.truncateBuffer = []
|
|
686
|
+
this.pendingTruncateRefetches.clear()
|
|
687
|
+
|
|
482
688
|
// Unload all subsets that this subscription loaded
|
|
483
689
|
// We pass the exact same LoadSubsetOptions we used for loadSubset
|
|
484
690
|
for (const options of this.loadedSubsets) {
|
package/src/collection/sync.ts
CHANGED
|
@@ -12,10 +12,11 @@ import { deepEquals } from '../utils'
|
|
|
12
12
|
import { LIVE_QUERY_INTERNAL } from '../query/live/internal.js'
|
|
13
13
|
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
|
14
14
|
import type {
|
|
15
|
-
|
|
15
|
+
ChangeMessageOrDeleteKeyMessage,
|
|
16
16
|
CleanupFn,
|
|
17
17
|
CollectionConfig,
|
|
18
18
|
LoadSubsetOptions,
|
|
19
|
+
OptimisticChangeMessage,
|
|
19
20
|
SyncConfigRes,
|
|
20
21
|
} from '../types'
|
|
21
22
|
import type { CollectionImpl } from './index.js'
|
|
@@ -94,7 +95,12 @@ export class CollectionSyncManager<
|
|
|
94
95
|
deletedKeys: new Set(),
|
|
95
96
|
})
|
|
96
97
|
},
|
|
97
|
-
write: (
|
|
98
|
+
write: (
|
|
99
|
+
messageWithOptionalKey: ChangeMessageOrDeleteKeyMessage<
|
|
100
|
+
TOutput,
|
|
101
|
+
TKey
|
|
102
|
+
>,
|
|
103
|
+
) => {
|
|
98
104
|
const pendingTransaction =
|
|
99
105
|
this.state.pendingSyncedTransactions[
|
|
100
106
|
this.state.pendingSyncedTransactions.length - 1
|
|
@@ -105,12 +111,18 @@ export class CollectionSyncManager<
|
|
|
105
111
|
if (pendingTransaction.committed) {
|
|
106
112
|
throw new SyncTransactionAlreadyCommittedWriteError()
|
|
107
113
|
}
|
|
108
|
-
const key = this.config.getKey(messageWithoutKey.value)
|
|
109
114
|
|
|
110
|
-
let
|
|
115
|
+
let key: TKey | undefined = undefined
|
|
116
|
+
if (`key` in messageWithOptionalKey) {
|
|
117
|
+
key = messageWithOptionalKey.key
|
|
118
|
+
} else {
|
|
119
|
+
key = this.config.getKey(messageWithOptionalKey.value)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let messageType = messageWithOptionalKey.type
|
|
111
123
|
|
|
112
124
|
// Check if an item with this key already exists when inserting
|
|
113
|
-
if (
|
|
125
|
+
if (messageWithOptionalKey.type === `insert`) {
|
|
114
126
|
const insertingIntoExistingSynced = this.state.syncedData.has(key)
|
|
115
127
|
const hasPendingDeleteForKey =
|
|
116
128
|
pendingTransaction.deletedKeys.has(key)
|
|
@@ -124,7 +136,7 @@ export class CollectionSyncManager<
|
|
|
124
136
|
const existingValue = this.state.syncedData.get(key)
|
|
125
137
|
if (
|
|
126
138
|
existingValue !== undefined &&
|
|
127
|
-
deepEquals(existingValue,
|
|
139
|
+
deepEquals(existingValue, messageWithOptionalKey.value)
|
|
128
140
|
) {
|
|
129
141
|
// The "insert" is an echo of a value we already have locally.
|
|
130
142
|
// Treat it as an update so we preserve optimistic intent without
|
|
@@ -142,11 +154,11 @@ export class CollectionSyncManager<
|
|
|
142
154
|
}
|
|
143
155
|
}
|
|
144
156
|
|
|
145
|
-
const message
|
|
146
|
-
...
|
|
157
|
+
const message = {
|
|
158
|
+
...messageWithOptionalKey,
|
|
147
159
|
type: messageType,
|
|
148
160
|
key,
|
|
149
|
-
}
|
|
161
|
+
} as OptimisticChangeMessage<TOutput, TKey>
|
|
150
162
|
pendingTransaction.operations.push(message)
|
|
151
163
|
|
|
152
164
|
if (messageType === `delete`) {
|
package/src/types.ts
CHANGED
|
@@ -328,7 +328,7 @@ export interface SyncConfig<
|
|
|
328
328
|
sync: (params: {
|
|
329
329
|
collection: Collection<T, TKey, any, any, any>
|
|
330
330
|
begin: () => void
|
|
331
|
-
write: (message:
|
|
331
|
+
write: (message: ChangeMessageOrDeleteKeyMessage<T, TKey>) => void
|
|
332
332
|
commit: () => void
|
|
333
333
|
markReady: () => void
|
|
334
334
|
truncate: () => void
|
|
@@ -361,12 +361,28 @@ export interface ChangeMessage<
|
|
|
361
361
|
metadata?: Record<string, unknown>
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
-
export
|
|
364
|
+
export type DeleteKeyMessage<TKey extends string | number = string | number> =
|
|
365
|
+
Omit<ChangeMessage<any, TKey>, `value` | `previousValue` | `type`> & {
|
|
366
|
+
type: `delete`
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export type ChangeMessageOrDeleteKeyMessage<
|
|
365
370
|
T extends object = Record<string, unknown>,
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
371
|
+
TKey extends string | number = string | number,
|
|
372
|
+
> = Omit<ChangeMessage<T>, `key`> | DeleteKeyMessage<TKey>
|
|
373
|
+
|
|
374
|
+
export type OptimisticChangeMessage<
|
|
375
|
+
T extends object = Record<string, unknown>,
|
|
376
|
+
TKey extends string | number = string | number,
|
|
377
|
+
> =
|
|
378
|
+
| (ChangeMessage<T> & {
|
|
379
|
+
// Is this change message part of an active transaction. Only applies to optimistic changes.
|
|
380
|
+
isActive?: boolean
|
|
381
|
+
})
|
|
382
|
+
| (DeleteKeyMessage<TKey> & {
|
|
383
|
+
// Is this change message part of an active transaction. Only applies to optimistic changes.
|
|
384
|
+
isActive?: boolean
|
|
385
|
+
})
|
|
370
386
|
|
|
371
387
|
/**
|
|
372
388
|
* The Standard Schema interface.
|
|
@@ -894,3 +910,6 @@ export type WritableDeep<T> = T extends BuiltIns
|
|
|
894
910
|
: T extends object
|
|
895
911
|
? WritableObjectDeep<T>
|
|
896
912
|
: unknown
|
|
913
|
+
|
|
914
|
+
export type MakeOptional<T, K extends keyof T> = Omit<T, K> &
|
|
915
|
+
Partial<Pick<T, K>>
|