jsonbadger 0.5.0 → 0.6.0
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/README.md +36 -18
- package/docs/api/connection.md +144 -0
- package/docs/api/delta-tracker.md +106 -0
- package/docs/api/document.md +77 -0
- package/docs/api/field-types.md +329 -0
- package/docs/api/index.md +35 -0
- package/docs/api/model.md +392 -0
- package/docs/api/query-builder.md +81 -0
- package/docs/api/schema.md +204 -0
- package/docs/architecture-flow.md +397 -0
- package/docs/examples.md +495 -218
- package/docs/jsonb-ops.md +171 -0
- package/docs/lifecycle/model-compilation.md +111 -0
- package/docs/lifecycle.md +146 -0
- package/docs/query-translation.md +11 -10
- package/package.json +10 -3
- package/src/connection/connect.js +12 -17
- package/src/connection/connection.js +128 -0
- package/src/connection/server-capabilities.js +60 -59
- package/src/constants/defaults.js +32 -19
- package/src/constants/{id-strategies.js → id-strategy.js} +28 -29
- package/src/constants/intake-mode.js +8 -0
- package/src/debug/debug-logger.js +17 -15
- package/src/errors/model-overwrite-error.js +25 -0
- package/src/errors/query-error.js +25 -23
- package/src/errors/validation-error.js +25 -23
- package/src/field-types/base-field-type.js +137 -140
- package/src/field-types/builtins/advanced.js +365 -365
- package/src/field-types/builtins/index.js +579 -585
- package/src/field-types/field-type-namespace.js +9 -0
- package/src/field-types/registry.js +149 -122
- package/src/index.js +26 -36
- package/src/migration/ensure-index.js +157 -154
- package/src/migration/ensure-schema.js +27 -15
- package/src/migration/ensure-table.js +44 -31
- package/src/migration/schema-indexes-resolver.js +8 -6
- package/src/model/document-instance.js +29 -540
- package/src/model/document.js +60 -0
- package/src/model/factory/constants.js +36 -0
- package/src/model/factory/index.js +58 -0
- package/src/model/model.js +875 -0
- package/src/model/operations/delete-one.js +39 -0
- package/src/model/operations/insert-one.js +35 -0
- package/src/model/operations/query-builder.js +132 -0
- package/src/model/operations/update-one.js +333 -0
- package/src/model/state.js +34 -0
- package/src/schema/field-definition-parser.js +213 -218
- package/src/schema/path-introspection.js +87 -82
- package/src/schema/schema-compiler.js +126 -212
- package/src/schema/schema.js +621 -138
- package/src/sql/index.js +17 -0
- package/src/sql/jsonb/ops.js +153 -0
- package/src/{query → sql/jsonb}/path-parser.js +54 -43
- package/src/sql/jsonb/read/elem-match.js +133 -0
- package/src/{query → sql/jsonb/read}/operators/contains.js +13 -7
- package/src/sql/jsonb/read/operators/elem-match.js +9 -0
- package/src/{query → sql/jsonb/read}/operators/has-all-keys.js +17 -11
- package/src/{query → sql/jsonb/read}/operators/has-any-keys.js +18 -11
- package/src/sql/jsonb/read/operators/has-key.js +12 -0
- package/src/{query → sql/jsonb/read}/operators/jsonpath-exists.js +22 -15
- package/src/{query → sql/jsonb/read}/operators/jsonpath-match.js +22 -15
- package/src/{query → sql/jsonb/read}/operators/size.js +23 -16
- package/src/sql/parameter-binder.js +18 -13
- package/src/sql/read/build-count-query.js +12 -0
- package/src/sql/read/build-find-query.js +25 -0
- package/src/sql/read/limit-skip.js +21 -0
- package/src/sql/read/sort.js +85 -0
- package/src/sql/read/where/base-fields.js +310 -0
- package/src/sql/read/where/casting.js +90 -0
- package/src/sql/read/where/context.js +79 -0
- package/src/sql/read/where/field-clause.js +58 -0
- package/src/sql/read/where/index.js +38 -0
- package/src/sql/read/where/operator-entries.js +29 -0
- package/src/{query → sql/read/where}/operators/all.js +16 -10
- package/src/sql/read/where/operators/eq.js +12 -0
- package/src/{query → sql/read/where}/operators/gt.js +23 -16
- package/src/{query → sql/read/where}/operators/gte.js +23 -16
- package/src/{query → sql/read/where}/operators/in.js +18 -12
- package/src/sql/read/where/operators/index.js +40 -0
- package/src/{query → sql/read/where}/operators/lt.js +23 -16
- package/src/{query → sql/read/where}/operators/lte.js +23 -16
- package/src/sql/read/where/operators/ne.js +12 -0
- package/src/{query → sql/read/where}/operators/nin.js +18 -12
- package/src/{query → sql/read/where}/operators/regex.js +14 -8
- package/src/sql/read/where/operators.js +126 -0
- package/src/sql/read/where/text-operators.js +83 -0
- package/src/sql/run.js +46 -0
- package/src/sql/write/build-delete-query.js +33 -0
- package/src/sql/write/build-insert-query.js +42 -0
- package/src/sql/write/build-update-query.js +65 -0
- package/src/utils/assert.js +34 -27
- package/src/utils/delta-tracker/.archive/1 tracker-redesign-codex-v2.md +250 -0
- package/src/utils/delta-tracker/.archive/1 tracker-redesign-gemini.md +101 -0
- package/src/utils/delta-tracker/.archive/2 evaluation by gemini.txt +65 -0
- package/src/utils/delta-tracker/.archive/2 evaluation by grok.txt +39 -0
- package/src/utils/delta-tracker/.archive/3 gemini evaluate grok.txt +37 -0
- package/src/utils/delta-tracker/.archive/3 grok evaluate gemini.txt +63 -0
- package/src/utils/delta-tracker/.archive/4 gemini veredict.txt +16 -0
- package/src/utils/delta-tracker/.archive/index.1.js +587 -0
- package/src/utils/delta-tracker/.archive/index.2.js +612 -0
- package/src/utils/delta-tracker/index.js +592 -0
- package/src/utils/dirty-tracker/inline.js +335 -0
- package/src/utils/dirty-tracker/instance.js +414 -0
- package/src/utils/dirty-tracker/static.js +343 -0
- package/src/utils/json-safe.js +13 -9
- package/src/utils/object-path.js +227 -33
- package/src/utils/object.js +408 -168
- package/src/utils/string.js +55 -0
- package/src/utils/value.js +169 -30
- package/docs/api.md +0 -152
- package/src/connection/disconnect.js +0 -16
- package/src/connection/pool-store.js +0 -46
- package/src/model/model-factory.js +0 -555
- package/src/query/limit-skip-compiler.js +0 -31
- package/src/query/operators/elem-match.js +0 -3
- package/src/query/operators/eq.js +0 -6
- package/src/query/operators/has-key.js +0 -6
- package/src/query/operators/index.js +0 -60
- package/src/query/operators/ne.js +0 -6
- package/src/query/query-builder.js +0 -93
- package/src/query/sort-compiler.js +0 -30
- package/src/query/where-compiler.js +0 -477
- package/src/sql/sql-runner.js +0 -31
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import {to_array} from '#src/utils/array.js';
|
|
2
|
+
import {deep_clone, is_function, is_not_object, is_object} from '#src/utils/value.js';
|
|
3
|
+
|
|
4
|
+
// DELTA-TRACKER v1
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a new state store instance that proxies a root object,
|
|
8
|
+
* optionally restricting tracking to specific child branches.
|
|
9
|
+
* Emits a structured NoSQL-style delta object natively.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} target
|
|
12
|
+
* @param {object} [options]
|
|
13
|
+
* @param {string[]} [options.track] - Array of top-level keys to track (e.g., ['data', 'fields'])
|
|
14
|
+
* @param {object} [options.watch]
|
|
15
|
+
* @param {function} [options.intercept_set]
|
|
16
|
+
* @returns {Proxy}
|
|
17
|
+
*/
|
|
18
|
+
function DeltaTracker(target, options = {}) {
|
|
19
|
+
const track_list = get_track_list(options);
|
|
20
|
+
const base_state = clone_tracked_state(target, track_list);
|
|
21
|
+
|
|
22
|
+
const store = {
|
|
23
|
+
base_state,
|
|
24
|
+
// State-collapsed delta tracking
|
|
25
|
+
delta_set: new Map(), // path -> next_value
|
|
26
|
+
delta_unset: new Set(), // paths that were deleted
|
|
27
|
+
replace_roots: new Map(), // root_key -> full_object (when tracked roots are replaced entirely)
|
|
28
|
+
watchers: [],
|
|
29
|
+
// Reuse nested proxies so repeated reads preserve referential equality.
|
|
30
|
+
proxy_cache: new WeakMap()
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let root_proxy;
|
|
34
|
+
const get_root = () => root_proxy;
|
|
35
|
+
|
|
36
|
+
// Build the intercepting proxy
|
|
37
|
+
root_proxy = build_proxy(target, target, store, get_root, options);
|
|
38
|
+
// Seed the cache with the root object so nested traversals can share proxy identity.
|
|
39
|
+
store.proxy_cache.set(target, root_proxy);
|
|
40
|
+
|
|
41
|
+
// Parse and bind Watchers
|
|
42
|
+
if(is_object(options.watch)) {
|
|
43
|
+
const watch_keys = Object.keys(options.watch);
|
|
44
|
+
let key_index = 0;
|
|
45
|
+
|
|
46
|
+
while(key_index < watch_keys.length) {
|
|
47
|
+
const watch_path = watch_keys[key_index];
|
|
48
|
+
add_watcher(store, target, root_proxy, watch_path, options.watch[watch_path]);
|
|
49
|
+
key_index += 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return root_proxy;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/*
|
|
57
|
+
* LAZY PROXY BUILDER
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build one lazy proxy for the provided object branch.
|
|
62
|
+
*
|
|
63
|
+
* @param {object} target_object
|
|
64
|
+
* @param {object} root_object
|
|
65
|
+
* @param {object} store
|
|
66
|
+
* @param {function(): Proxy} get_root
|
|
67
|
+
* @param {object} [options]
|
|
68
|
+
* @returns {Proxy}
|
|
69
|
+
*/
|
|
70
|
+
function build_proxy(target_object, root_object, store, get_root, options = {}) {
|
|
71
|
+
const track_list = get_track_list(options);
|
|
72
|
+
const base_path = options.base_path || '';
|
|
73
|
+
let intercept_set = (path, next_value) => next_value;
|
|
74
|
+
|
|
75
|
+
if(is_function(options.intercept_set)) {
|
|
76
|
+
intercept_set = options.intercept_set;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const internal_methods = Object.assign(Object.create(null), {
|
|
80
|
+
$has_changes: () => {
|
|
81
|
+
return has_changes(store);
|
|
82
|
+
},
|
|
83
|
+
$get_delta: () => {
|
|
84
|
+
return get_delta(store);
|
|
85
|
+
},
|
|
86
|
+
$reset_changes: () => {
|
|
87
|
+
return reset_changes(store, get_root());
|
|
88
|
+
},
|
|
89
|
+
$rebase_changes: () => {
|
|
90
|
+
return rebase_changes(store, root_object, options);
|
|
91
|
+
},
|
|
92
|
+
$watch: (path, watch_options) => {
|
|
93
|
+
return add_watcher(store, root_object, get_root(), path, watch_options);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return new Proxy(target_object, {
|
|
98
|
+
get(target, prop) {
|
|
99
|
+
// Do not proxy internal JS symbols
|
|
100
|
+
if(typeof prop === 'symbol') {
|
|
101
|
+
return target[prop];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Root level: allow access to tracker methods and pass through untracked branches
|
|
105
|
+
if(base_path === '') {
|
|
106
|
+
// Keep helper methods only at the root so tracked branches stay free of tracker
|
|
107
|
+
// method names and can be treated as normal application data.
|
|
108
|
+
const internal_method = internal_methods[prop];
|
|
109
|
+
|
|
110
|
+
if(internal_method) {
|
|
111
|
+
return internal_method;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if(prop in target && is_function(target[prop])) {
|
|
115
|
+
return target[prop];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Keep untracked root branches raw so only the declared tracked surface participates
|
|
119
|
+
// in delta bookkeeping and helper-method interception.
|
|
120
|
+
if(track_list && !track_list.includes(prop)) {
|
|
121
|
+
return target[prop];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const value = target[prop];
|
|
126
|
+
|
|
127
|
+
// Lazy-proxy nested objects on access
|
|
128
|
+
if(is_object(value)) {
|
|
129
|
+
const cached_proxy = store.proxy_cache.get(value);
|
|
130
|
+
|
|
131
|
+
if(cached_proxy) {
|
|
132
|
+
// Reuse the same proxy instance so `tracker.user === tracker.user` stays true.
|
|
133
|
+
return cached_proxy;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const next_path = base_path === '' ? prop : `${base_path}.${prop}`;
|
|
137
|
+
const next_options = {...options, base_path: next_path};
|
|
138
|
+
const nested_proxy = build_proxy(value, root_object, store, get_root, next_options);
|
|
139
|
+
|
|
140
|
+
store.proxy_cache.set(value, nested_proxy);
|
|
141
|
+
|
|
142
|
+
return nested_proxy;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return value;
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
set(target, prop, value) {
|
|
149
|
+
if(typeof prop === 'symbol') {
|
|
150
|
+
target[prop] = value;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Do not track mutations to functions/methods
|
|
155
|
+
if(base_path === '' && is_function(target[prop])) {
|
|
156
|
+
target[prop] = value;
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Bypass tracking for untracked top-level keys
|
|
161
|
+
if(base_path === '' && track_list && !track_list.includes(prop)) {
|
|
162
|
+
target[prop] = value;
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const full_path = base_path === '' ? prop : `${base_path}.${prop}`;
|
|
167
|
+
const next_value = intercept_set(full_path, value);
|
|
168
|
+
const old_value = target[prop];
|
|
169
|
+
|
|
170
|
+
// Delta state is snapshot-based where possible to naturally eliminate redundant writes
|
|
171
|
+
const original_value = read_path(store.base_state, full_path);
|
|
172
|
+
|
|
173
|
+
// Apply the mutation
|
|
174
|
+
target[prop] = next_value;
|
|
175
|
+
|
|
176
|
+
// Is this an assignment replacing a tracked root completely? (e.g. `document.data = {...}`)
|
|
177
|
+
if(base_path === '' && track_list && track_list.includes(prop)) {
|
|
178
|
+
store.replace_roots.set(prop, next_value);
|
|
179
|
+
clear_nested_deltas(store, full_path);
|
|
180
|
+
|
|
181
|
+
} else {
|
|
182
|
+
// Standard nested operation tracking
|
|
183
|
+
if(original_value === next_value) {
|
|
184
|
+
// Reverted to baseline - remove from delta sets entirely
|
|
185
|
+
store.delta_set.delete(full_path);
|
|
186
|
+
store.delta_unset.delete(full_path);
|
|
187
|
+
} else {
|
|
188
|
+
// Cross-cancellation: setting a key overrides any unsets for it
|
|
189
|
+
store.delta_set.set(full_path, next_value);
|
|
190
|
+
store.delta_unset.delete(full_path);
|
|
191
|
+
clear_nested_deltas(store, full_path); // Overwriting parent nullifies child ops
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Trigger Watchers
|
|
196
|
+
if(old_value !== next_value) {
|
|
197
|
+
check_watchers(store, full_path, old_value, root_object, get_root());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return true;
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
deleteProperty(target, prop) {
|
|
204
|
+
if(typeof prop === 'symbol') {
|
|
205
|
+
return Reflect.deleteProperty(target, prop);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Bypass tracking for untracked top-level keys
|
|
209
|
+
if(base_path === '' && track_list && !track_list.includes(prop)) {
|
|
210
|
+
return Reflect.deleteProperty(target, prop);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const full_path = base_path === '' ? prop : `${base_path}.${prop}`;
|
|
214
|
+
const old_value = target[prop];
|
|
215
|
+
|
|
216
|
+
// Apply the mutation
|
|
217
|
+
const deleted = Reflect.deleteProperty(target, prop);
|
|
218
|
+
|
|
219
|
+
if(deleted) {
|
|
220
|
+
// Are we deleting a full tracked root? (e.g. `delete document.data`)
|
|
221
|
+
if(base_path === '' && track_list && track_list.includes(prop)) {
|
|
222
|
+
store.replace_roots.delete(prop);
|
|
223
|
+
store.delta_unset.add(full_path);
|
|
224
|
+
clear_nested_deltas(store, full_path);
|
|
225
|
+
|
|
226
|
+
} else {
|
|
227
|
+
// Cross-cancellation: deleting a key nullifies any pending sets for it
|
|
228
|
+
store.delta_unset.add(full_path);
|
|
229
|
+
store.delta_set.delete(full_path);
|
|
230
|
+
clear_nested_deltas(store, full_path);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Trigger Watchers (old_value will be the removed object, new value will be undefined)
|
|
234
|
+
check_watchers(store, full_path, old_value, root_object, get_root());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return deleted;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/*
|
|
243
|
+
* STATE MANAGEMENT HELPERS
|
|
244
|
+
*/
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Wipes out any pending set/unset instructions for children of a path.
|
|
248
|
+
* Used when a parent object is overwritten or deleted natively collapsing state.
|
|
249
|
+
*
|
|
250
|
+
* @param {object} store
|
|
251
|
+
* @param {string} parent_path
|
|
252
|
+
* @returns {void}
|
|
253
|
+
*/
|
|
254
|
+
function clear_nested_deltas(store, parent_path) {
|
|
255
|
+
const prefix = `${parent_path}.`;
|
|
256
|
+
|
|
257
|
+
for(const key of store.delta_set.keys()) {
|
|
258
|
+
if(key.startsWith(prefix)) {
|
|
259
|
+
store.delta_set.delete(key);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for(const key of store.delta_unset) {
|
|
264
|
+
if(key.startsWith(prefix)) {
|
|
265
|
+
store.delta_unset.delete(key);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Normalize tracker options into a stable tracked-root list.
|
|
272
|
+
*
|
|
273
|
+
* @param {object} options
|
|
274
|
+
* @param {string|string[]} [options.track]
|
|
275
|
+
* @returns {string[]|null}
|
|
276
|
+
*/
|
|
277
|
+
function get_track_list(options) {
|
|
278
|
+
return options.track === undefined ? null : to_array(options.track);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Clone either the full source object or only the tracked root keys.
|
|
283
|
+
*
|
|
284
|
+
* @param {object} source_object
|
|
285
|
+
* @param {string[]|null} track_list
|
|
286
|
+
* @returns {object}
|
|
287
|
+
*/
|
|
288
|
+
function clone_tracked_state(source_object, track_list) {
|
|
289
|
+
if(!track_list) {
|
|
290
|
+
// Full tracking can reuse the deep clone directly instead of copying into another object.
|
|
291
|
+
return deep_clone(source_object);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const cloned_state = {};
|
|
295
|
+
let key_index = 0;
|
|
296
|
+
|
|
297
|
+
while(key_index < track_list.length) {
|
|
298
|
+
const key = track_list[key_index];
|
|
299
|
+
|
|
300
|
+
if(key in source_object) {
|
|
301
|
+
cloned_state[key] = deep_clone(source_object[key]);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
key_index += 1;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return cloned_state;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Check whether the tracker currently holds any pending changes.
|
|
312
|
+
*
|
|
313
|
+
* @param {object} store
|
|
314
|
+
* @returns {boolean}
|
|
315
|
+
*/
|
|
316
|
+
function has_changes(store) {
|
|
317
|
+
if(!store) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return store.delta_set.size > 0 || store.delta_unset.size > 0 || store.replace_roots.size > 0;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Return the current delta snapshot for the tracked roots.
|
|
326
|
+
*
|
|
327
|
+
* @param {object} store
|
|
328
|
+
* @returns {{replace_roots: object, set: object, unset: string[]}}
|
|
329
|
+
*/
|
|
330
|
+
function get_delta(store) {
|
|
331
|
+
if(!store) {
|
|
332
|
+
return {replace_roots: {}, set: {}, unset: []};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
replace_roots: Object.fromEntries(store.replace_roots),
|
|
337
|
+
set: Object.fromEntries(store.delta_set),
|
|
338
|
+
unset: Array.from(store.delta_unset)
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Reset tracked roots back to the current rebased snapshot.
|
|
344
|
+
*
|
|
345
|
+
* @param {object} store
|
|
346
|
+
* @param {Proxy} proxy
|
|
347
|
+
* @returns {void}
|
|
348
|
+
*/
|
|
349
|
+
function reset_changes(store, proxy) {
|
|
350
|
+
const original = store.base_state;
|
|
351
|
+
const original_keys = Object.keys(original);
|
|
352
|
+
let key_index = 0;
|
|
353
|
+
|
|
354
|
+
// Resetting through the proxy triggers setters naturally, effectively
|
|
355
|
+
// reverting state and wiping out pending deltas through the snapshot diff.
|
|
356
|
+
while(key_index < original_keys.length) {
|
|
357
|
+
const key = original_keys[key_index];
|
|
358
|
+
|
|
359
|
+
if(!is_function(original[key])) {
|
|
360
|
+
proxy[key] = deep_clone(original[key]);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
key_index += 1;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Remove any fully replaced roots that didn't exist in the baseline
|
|
367
|
+
for(const key of store.replace_roots.keys()) {
|
|
368
|
+
if(!(key in original)) {
|
|
369
|
+
delete proxy[key];
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
store.delta_set.clear();
|
|
374
|
+
store.delta_unset.clear();
|
|
375
|
+
store.replace_roots.clear();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Rebase the tracker snapshot against the current root object state.
|
|
380
|
+
*
|
|
381
|
+
* @param {object} store
|
|
382
|
+
* @param {object} root_object
|
|
383
|
+
* @param {object} options
|
|
384
|
+
* @returns {void}
|
|
385
|
+
*/
|
|
386
|
+
function rebase_changes(store, root_object, options) {
|
|
387
|
+
const track_list = get_track_list(options);
|
|
388
|
+
store.base_state = clone_tracked_state(root_object, track_list);
|
|
389
|
+
|
|
390
|
+
store.delta_set.clear();
|
|
391
|
+
store.delta_unset.clear();
|
|
392
|
+
store.replace_roots.clear();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/*
|
|
396
|
+
* WATCHER HELPERS
|
|
397
|
+
*/
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Register one watcher against the tracked root proxy.
|
|
401
|
+
*
|
|
402
|
+
* @param {object} store
|
|
403
|
+
* @param {object} root_object
|
|
404
|
+
* @param {Proxy} proxy
|
|
405
|
+
* @param {string} path
|
|
406
|
+
* @param {object|function} options
|
|
407
|
+
* @returns {function}
|
|
408
|
+
*/
|
|
409
|
+
function add_watcher(store, root_object, proxy, path, options) {
|
|
410
|
+
const handler = is_function(options) ? options : () => {};
|
|
411
|
+
const config = is_object(options) ? options : {handler};
|
|
412
|
+
|
|
413
|
+
const watcher = {
|
|
414
|
+
path,
|
|
415
|
+
handler: config.handler,
|
|
416
|
+
deep: config.deep === true,
|
|
417
|
+
once: config.once === true,
|
|
418
|
+
active: true
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
store.watchers.push(watcher);
|
|
422
|
+
|
|
423
|
+
if(config.immediate) {
|
|
424
|
+
const initial_value = read_path(root_object, path);
|
|
425
|
+
watcher.handler.call(proxy, initial_value, undefined);
|
|
426
|
+
|
|
427
|
+
if(watcher.once) {
|
|
428
|
+
watcher.active = false;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Return the closure to unwatch
|
|
433
|
+
return () => {
|
|
434
|
+
watcher.active = false;
|
|
435
|
+
const index = store.watchers.indexOf(watcher);
|
|
436
|
+
|
|
437
|
+
if(index !== -1) {
|
|
438
|
+
store.watchers.splice(index, 1);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const pending_watchers = new Map();
|
|
444
|
+
let is_flushing = false;
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Evaluate which watchers should react to one mutated path.
|
|
448
|
+
*
|
|
449
|
+
* @param {object} store
|
|
450
|
+
* @param {string} mutated_path
|
|
451
|
+
* @param {*} old_value
|
|
452
|
+
* @param {object} root_object
|
|
453
|
+
* @param {Proxy} root_proxy
|
|
454
|
+
* @returns {void}
|
|
455
|
+
*/
|
|
456
|
+
function check_watchers(store, mutated_path, old_value, root_object, root_proxy) {
|
|
457
|
+
const watchers = store.watchers;
|
|
458
|
+
let watcher_index = 0;
|
|
459
|
+
|
|
460
|
+
while(watcher_index < watchers.length) {
|
|
461
|
+
const watcher = watchers[watcher_index];
|
|
462
|
+
if(!watcher.active) {
|
|
463
|
+
watcher_index += 1;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let should_trigger = false;
|
|
468
|
+
let handler_new_value = undefined;
|
|
469
|
+
let handler_old_value = old_value;
|
|
470
|
+
|
|
471
|
+
// Exact path match
|
|
472
|
+
if(watcher.path === mutated_path) {
|
|
473
|
+
should_trigger = true;
|
|
474
|
+
handler_new_value = read_path(root_object, mutated_path);
|
|
475
|
+
}
|
|
476
|
+
// Deep mutation (e.g., watching 'user', mutated 'user.name')
|
|
477
|
+
else if(watcher.deep && mutated_path.startsWith(watcher.path + '.')) {
|
|
478
|
+
should_trigger = true;
|
|
479
|
+
// Deep child writes mutate the same parent object in place, so there is no separate
|
|
480
|
+
// pre-mutation parent snapshot to pass through here.
|
|
481
|
+
handler_new_value = read_path(root_object, watcher.path);
|
|
482
|
+
handler_old_value = handler_new_value;
|
|
483
|
+
}
|
|
484
|
+
// Parent replacement (e.g., watching 'user.name', mutated 'user')
|
|
485
|
+
else if(watcher.path.startsWith(mutated_path + '.')) {
|
|
486
|
+
should_trigger = true;
|
|
487
|
+
handler_new_value = read_path(root_object, watcher.path);
|
|
488
|
+
|
|
489
|
+
// Attempt to extract the old nested value from the replaced parent object
|
|
490
|
+
const nested_path = watcher.path.substring(mutated_path.length + 1);
|
|
491
|
+
handler_old_value = read_path(old_value, nested_path);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if(should_trigger) {
|
|
495
|
+
queue_watcher(watcher, handler_new_value, handler_old_value, root_proxy);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
watcher_index += 1;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Queue one watcher callback into the current microtask batch.
|
|
504
|
+
*
|
|
505
|
+
* @param {object} watcher
|
|
506
|
+
* @param {*} new_value
|
|
507
|
+
* @param {*} old_value
|
|
508
|
+
* @param {*} context
|
|
509
|
+
* @returns {void}
|
|
510
|
+
*/
|
|
511
|
+
function queue_watcher(watcher, new_value, old_value, context) {
|
|
512
|
+
// Deduplicate watchers in the same tick.
|
|
513
|
+
// If it exists, we update to the latest new_value, but keep the initial old_value of this tick.
|
|
514
|
+
if(pending_watchers.has(watcher)) {
|
|
515
|
+
pending_watchers.get(watcher).new_value = new_value;
|
|
516
|
+
} else {
|
|
517
|
+
pending_watchers.set(watcher, {new_value, old_value, context});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if(is_flushing) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
is_flushing = true;
|
|
525
|
+
|
|
526
|
+
// Batch watcher delivery into one microtask so multiple synchronous writes collapse into a
|
|
527
|
+
// single callback pass with the latest value and the first old value from that tick.
|
|
528
|
+
Promise.resolve().then(() => {
|
|
529
|
+
const jobs = Array.from(pending_watchers.entries());
|
|
530
|
+
pending_watchers.clear();
|
|
531
|
+
is_flushing = false;
|
|
532
|
+
|
|
533
|
+
let job_index = 0;
|
|
534
|
+
while(job_index < jobs.length) {
|
|
535
|
+
const job_watcher = jobs[job_index][0];
|
|
536
|
+
const job_args = jobs[job_index][1];
|
|
537
|
+
|
|
538
|
+
if(job_watcher.active) {
|
|
539
|
+
job_watcher.handler.call(
|
|
540
|
+
job_args.context,
|
|
541
|
+
job_args.new_value,
|
|
542
|
+
job_args.old_value
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
if(job_watcher.once) {
|
|
546
|
+
job_watcher.active = false; // Mark inactive after first run
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
job_index += 1;
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/*
|
|
556
|
+
* PATH HELPERS
|
|
557
|
+
*/
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Read one nested value by dot-notation path.
|
|
561
|
+
*
|
|
562
|
+
* @param {object} root_object
|
|
563
|
+
* @param {string} dot_path
|
|
564
|
+
* @returns {*}
|
|
565
|
+
*/
|
|
566
|
+
function read_path(root_object, dot_path) {
|
|
567
|
+
if(!dot_path) {
|
|
568
|
+
return root_object;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const segments = dot_path.split('.');
|
|
572
|
+
let current_value = root_object;
|
|
573
|
+
let segment_index = 0;
|
|
574
|
+
|
|
575
|
+
while(segment_index < segments.length) {
|
|
576
|
+
if(is_not_object(current_value)) {
|
|
577
|
+
return undefined;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
current_value = current_value[segments[segment_index]];
|
|
581
|
+
segment_index += 1;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return current_value;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export default DeltaTracker;
|