@woltz/rich-domain 0.2.2 → 1.1.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/CHANGELOG.md +23 -75
- package/LICENSE +20 -20
- package/README.md +37 -20
- package/dist/base-entity.d.ts +2 -2
- package/dist/base-entity.d.ts.map +1 -1
- package/dist/base-entity.js +6 -4
- package/dist/base-entity.js.map +1 -1
- package/dist/criteria.d.ts +5 -11
- package/dist/criteria.d.ts.map +1 -1
- package/dist/criteria.js +4 -3
- package/dist/criteria.js.map +1 -1
- package/dist/deep-proxy.d.ts +3 -1
- package/dist/deep-proxy.d.ts.map +1 -1
- package/dist/deep-proxy.js +116 -29
- package/dist/deep-proxy.js.map +1 -1
- package/dist/domain-event-bus.d.ts +5 -6
- package/dist/domain-event-bus.d.ts.map +1 -1
- package/dist/domain-event-bus.js +3 -11
- package/dist/domain-event-bus.js.map +1 -1
- package/dist/domain-event.d.ts +1 -31
- package/dist/domain-event.d.ts.map +1 -1
- package/dist/domain-event.js +2 -1
- package/dist/domain-event.js.map +1 -1
- package/dist/entity.d.ts +2 -2
- package/dist/entity.js +1 -1
- package/dist/exceptions.d.ts +251 -0
- package/dist/exceptions.d.ts.map +1 -0
- package/dist/exceptions.js +321 -0
- package/dist/exceptions.js.map +1 -0
- package/dist/id.d.ts +3 -3
- package/dist/id.d.ts.map +1 -1
- package/dist/id.js +15 -4
- package/dist/id.js.map +1 -1
- package/dist/index.d.ts +2 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -8
- package/dist/index.js.map +1 -1
- package/dist/paginated-result.d.ts.map +1 -1
- package/dist/paginated-result.js +12 -1
- package/dist/paginated-result.js.map +1 -1
- package/dist/repository/index.d.ts +2 -39
- package/dist/repository/index.d.ts.map +1 -1
- package/dist/repository/index.js +2 -39
- package/dist/repository/index.js.map +1 -1
- package/dist/repository/unit-of-work.d.ts +0 -11
- package/dist/repository/unit-of-work.d.ts.map +1 -1
- package/dist/repository/unit-of-work.js +0 -35
- package/dist/repository/unit-of-work.js.map +1 -1
- package/dist/types/criteria.d.ts +6 -2
- package/dist/types/criteria.d.ts.map +1 -1
- package/dist/types/criteria.js +1 -1
- package/dist/types/criteria.js.map +1 -1
- package/dist/types/domain-event.d.ts +32 -0
- package/dist/types/domain-event.d.ts.map +1 -0
- package/dist/types/domain-event.js +2 -0
- package/dist/types/domain-event.js.map +1 -0
- package/dist/types/domain.d.ts +2 -2
- package/dist/types/domain.d.ts.map +1 -1
- package/dist/types/history-tracker.d.ts +1 -1
- package/dist/types/history-tracker.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/value-object.d.ts +1 -1
- package/dist/value-object.d.ts.map +1 -1
- package/dist/value-object.js +2 -5
- package/dist/value-object.js.map +1 -1
- package/eslint.config.js +3 -3
- package/jest.config.js +1 -1
- package/package.json +14 -20
- package/src/base-entity.ts +6 -5
- package/src/criteria.ts +11 -11
- package/src/deep-proxy.ts +447 -339
- package/src/domain-event-bus.ts +152 -166
- package/src/domain-event.ts +53 -90
- package/src/entity.ts +16 -16
- package/src/exceptions.ts +435 -0
- package/src/id.ts +107 -94
- package/src/index.ts +26 -9
- package/src/paginated-result.ts +14 -1
- package/src/repository/index.ts +2 -44
- package/src/repository/unit-of-work.ts +1 -44
- package/src/types/criteria.ts +7 -2
- package/src/types/domain-event.ts +38 -0
- package/src/types/domain.ts +2 -3
- package/src/types/history-tracker.ts +1 -1
- package/src/types/index.ts +1 -0
- package/src/validation-error.ts +97 -97
- package/src/value-object.ts +3 -6
- package/tests/criteria.test.ts +8 -0
- package/tests/domain-events.test.ts +431 -445
- package/tests/entity-validation.test.ts +2 -2
- package/tests/entity.test.ts +33 -33
- package/tests/history-tracker.spec.ts +57 -17
- package/tests/id.test.ts +341 -341
- package/tests/repository.test.ts +8 -4
- package/tests/to-json.test.ts +103 -91
- package/tests/utils.ts +254 -151
- package/tests/value-object-validation.test.ts +0 -9
- package/tests/value-objects.test.ts +52 -52
- package/tsconfig.json +2 -24
- package/.github/workflows/ci.yml +0 -40
- package/.husky/commit-msg +0 -1
- package/.husky/pre-commit +0 -1
- package/.vscode/settings.json +0 -3
- package/commitlint.config.js +0 -23
- package/dist/filtering.d.ts +0 -107
- package/dist/filtering.d.ts.map +0 -1
- package/dist/filtering.js +0 -202
- package/dist/filtering.js.map +0 -1
- package/dist/ordering.d.ts +0 -93
- package/dist/ordering.d.ts.map +0 -1
- package/dist/ordering.js +0 -154
- package/dist/ordering.js.map +0 -1
- package/dist/pagination.d.ts +0 -218
- package/dist/pagination.d.ts.map +0 -1
- package/dist/pagination.js +0 -281
- package/dist/pagination.js.map +0 -1
- package/dist/repository/in-memory-repository.d.ts +0 -50
- package/dist/repository/in-memory-repository.d.ts.map +0 -1
- package/dist/repository/in-memory-repository.js +0 -93
- package/dist/repository/in-memory-repository.js.map +0 -1
- package/dist/repository/mapper.d.ts +0 -56
- package/dist/repository/mapper.d.ts.map +0 -1
- package/dist/repository/mapper.js +0 -15
- package/dist/repository/mapper.js.map +0 -1
- package/dist/repository/types.d.ts +0 -87
- package/dist/repository/types.d.ts.map +0 -1
- package/dist/repository/types.js +0 -6
- package/dist/repository/types.js.map +0 -1
- package/dist/repository.d.ts +0 -2
- package/dist/repository.d.ts.map +0 -1
- package/dist/repository.js +0 -21
- package/dist/repository.js.map +0 -1
- package/dist/specification.d.ts +0 -102
- package/dist/specification.d.ts.map +0 -1
- package/dist/specification.js +0 -187
- package/dist/specification.js.map +0 -1
- package/dist/types/repository.d.ts +0 -43
- package/dist/types/repository.d.ts.map +0 -1
- package/dist/types/repository.js +0 -2
- package/dist/types/repository.js.map +0 -1
- package/dist/types.d.ts +0 -88
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -12
- package/dist/types.js.map +0 -1
- package/src/repository/in-memory-repository.ts +0 -116
package/src/deep-proxy.ts
CHANGED
|
@@ -1,339 +1,447 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// Deep Proxy - Core Change Tracking
|
|
3
|
-
// ============================================================================
|
|
4
|
-
|
|
5
|
-
import { Id } from
|
|
6
|
-
import { HistoryEntry } from
|
|
7
|
-
|
|
8
|
-
export class DeepProxy {
|
|
9
|
-
private history: HistoryEntry[] = [];
|
|
10
|
-
private subscribers: Map<string, Set<Function>> = new Map();
|
|
11
|
-
private originalValues: Map<string, any> = new Map();
|
|
12
|
-
private trackedArraysCloned: Map<string, any[]> = new Map();
|
|
13
|
-
private trackedArraysOriginal: Map<string, any[]> = new Map();
|
|
14
|
-
private globalSubscribers: Set<Function> = new Set();
|
|
15
|
-
private updateInProgress: boolean = false;
|
|
16
|
-
private pendingNotifications: Array<() => void> = [];
|
|
17
|
-
|
|
18
|
-
constructor(
|
|
19
|
-
private target: any,
|
|
20
|
-
private path: string =
|
|
21
|
-
private rootProxy?: DeepProxy
|
|
22
|
-
) {
|
|
23
|
-
if (!rootProxy) this.rootProxy = this;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
createProxy(): any {
|
|
27
|
-
const handler: ProxyHandler<any> = {
|
|
28
|
-
get: (target, prop, receiver) => {
|
|
29
|
-
const value = Reflect.get(target, prop, receiver);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
prop ===
|
|
38
|
-
prop ===
|
|
39
|
-
prop ===
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.rootProxy
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
this.rootProxy
|
|
96
|
-
this.rootProxy!.
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
this.rootProxy
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
private
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
this.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Deep Proxy - Core Change Tracking
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { Id } from "./id";
|
|
6
|
+
import { HistoryEntry } from "./types";
|
|
7
|
+
|
|
8
|
+
export class DeepProxy {
|
|
9
|
+
private history: HistoryEntry[] = [];
|
|
10
|
+
private subscribers: Map<string, Set<Function>> = new Map();
|
|
11
|
+
private originalValues: Map<string, any> = new Map();
|
|
12
|
+
private trackedArraysCloned: Map<string, any[]> = new Map();
|
|
13
|
+
private trackedArraysOriginal: Map<string, any[]> = new Map();
|
|
14
|
+
private globalSubscribers: Set<Function> = new Set();
|
|
15
|
+
private updateInProgress: boolean = false;
|
|
16
|
+
private pendingNotifications: Array<() => void> = [];
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
private target: any,
|
|
20
|
+
private path: string = "",
|
|
21
|
+
private rootProxy?: DeepProxy
|
|
22
|
+
) {
|
|
23
|
+
if (!rootProxy) this.rootProxy = this;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
createProxy(): any {
|
|
27
|
+
const handler: ProxyHandler<any> = {
|
|
28
|
+
get: (target, prop, receiver) => {
|
|
29
|
+
const value = Reflect.get(target, prop, receiver);
|
|
30
|
+
if (!this.rootProxy) throw new Error("Root proxy is required");
|
|
31
|
+
|
|
32
|
+
const currentPath = this.path
|
|
33
|
+
? `${this.path}.${String(prop)}`
|
|
34
|
+
: String(prop);
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
prop === "__isProxy" ||
|
|
38
|
+
prop === "__originalTarget" ||
|
|
39
|
+
prop === "__path" ||
|
|
40
|
+
prop === "constructor" ||
|
|
41
|
+
prop === "prototype"
|
|
42
|
+
) {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (typeof value === "function") return value.bind(target);
|
|
47
|
+
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
if (!this.rootProxy.trackedArraysCloned.has(currentPath)) {
|
|
50
|
+
this.rootProxy.storeArrayState(currentPath, value);
|
|
51
|
+
}
|
|
52
|
+
return this.createArrayProxy(value, currentPath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (value && typeof value === "object" && !value.__isProxy) {
|
|
56
|
+
const nestedProxy = new DeepProxy(value, currentPath, this.rootProxy);
|
|
57
|
+
return nestedProxy.createProxy();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return value;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
set: (target, prop, newValue, receiver) => {
|
|
64
|
+
const currentPath = this.path
|
|
65
|
+
? `${this.path}.${String(prop)}`
|
|
66
|
+
: String(prop);
|
|
67
|
+
if (!this.rootProxy) throw new Error("Root proxy is required");
|
|
68
|
+
|
|
69
|
+
const oldValue = Reflect.get(target, prop, receiver);
|
|
70
|
+
const isArrayAssignment = Array.isArray(newValue);
|
|
71
|
+
|
|
72
|
+
if (!isArrayAssignment && oldValue === newValue) return true;
|
|
73
|
+
|
|
74
|
+
if (!this.rootProxy.originalValues.has(currentPath)) {
|
|
75
|
+
this.rootProxy.originalValues.set(currentPath, oldValue);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.rootProxy.history.push({
|
|
79
|
+
path: currentPath,
|
|
80
|
+
previousValue: oldValue,
|
|
81
|
+
currentValue: newValue,
|
|
82
|
+
timestamp: Date.now(),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const result = Reflect.set(target, prop, newValue, receiver);
|
|
86
|
+
|
|
87
|
+
if (isArrayAssignment) {
|
|
88
|
+
if (!this.rootProxy.trackedArraysCloned.has(currentPath)) {
|
|
89
|
+
if (Array.isArray(oldValue)) {
|
|
90
|
+
this.rootProxy.storeArrayState(currentPath, oldValue);
|
|
91
|
+
} else {
|
|
92
|
+
this.rootProxy.storeArrayState(currentPath, []);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
this.rootProxy.scheduleNotification(() =>
|
|
96
|
+
this.rootProxy!.notifyArrayChange(currentPath, newValue)
|
|
97
|
+
);
|
|
98
|
+
} else {
|
|
99
|
+
this.rootProxy.scheduleNotification(() =>
|
|
100
|
+
this.rootProxy!.notifySubscribers(currentPath, oldValue, newValue)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Notify global subscribers
|
|
105
|
+
this.rootProxy.scheduleNotification(() =>
|
|
106
|
+
this.rootProxy!.notifyGlobalSubscribers()
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
this.rootProxy.flushNotifications();
|
|
110
|
+
|
|
111
|
+
return result;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const proxy = new Proxy(this.target, handler);
|
|
116
|
+
Object.defineProperty(proxy, "__isProxy", { value: true, writable: false });
|
|
117
|
+
return proxy;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private scheduleNotification(notification: () => void): void {
|
|
121
|
+
this.pendingNotifications.push(notification);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private flushNotifications(): void {
|
|
125
|
+
if (this.updateInProgress) return;
|
|
126
|
+
|
|
127
|
+
this.updateInProgress = true;
|
|
128
|
+
try {
|
|
129
|
+
const notifications = [...this.pendingNotifications];
|
|
130
|
+
this.pendingNotifications = [];
|
|
131
|
+
notifications.forEach((notify) => notify());
|
|
132
|
+
} finally {
|
|
133
|
+
this.updateInProgress = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private storeArrayState(path: string, arr: any[]): void {
|
|
138
|
+
this.trackedArraysCloned.set(path, this.cloneArray(arr));
|
|
139
|
+
this.trackedArraysOriginal.set(path, arr.slice());
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private createArrayProxy(array: any[], path: string): any[] {
|
|
143
|
+
const self = this;
|
|
144
|
+
return new Proxy(array, {
|
|
145
|
+
get(target, prop, receiver) {
|
|
146
|
+
const value = Reflect.get(target, prop, receiver);
|
|
147
|
+
if (!self.rootProxy) throw new Error("Root proxy is required");
|
|
148
|
+
|
|
149
|
+
if (typeof value === "function") {
|
|
150
|
+
const mutatingMethods = [
|
|
151
|
+
"push",
|
|
152
|
+
"pop",
|
|
153
|
+
"shift",
|
|
154
|
+
"unshift",
|
|
155
|
+
"splice",
|
|
156
|
+
"sort",
|
|
157
|
+
"reverse",
|
|
158
|
+
];
|
|
159
|
+
if (mutatingMethods.includes(String(prop))) {
|
|
160
|
+
return function (...args: any[]) {
|
|
161
|
+
// Capture state before mutation
|
|
162
|
+
if (!self.rootProxy) throw new Error("Root proxy is required");
|
|
163
|
+
const oldArray = target.slice();
|
|
164
|
+
|
|
165
|
+
const result = value.apply(target, args);
|
|
166
|
+
|
|
167
|
+
// Add to history
|
|
168
|
+
self.rootProxy.history.push({
|
|
169
|
+
path: path,
|
|
170
|
+
previousValue: oldArray,
|
|
171
|
+
currentValue: target.slice(),
|
|
172
|
+
timestamp: Date.now(),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (!self.rootProxy.trackedArraysCloned.has(path)) {
|
|
176
|
+
self.rootProxy.storeArrayState(path, target);
|
|
177
|
+
}
|
|
178
|
+
self.rootProxy.notifyArrayChange(path, target);
|
|
179
|
+
self.rootProxy.notifyGlobalSubscribers();
|
|
180
|
+
return result;
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return value.bind(target);
|
|
184
|
+
}
|
|
185
|
+
if (typeof value === "object" && value !== null && !value.__isProxy) {
|
|
186
|
+
const nestedPath = `${path}[${String(prop)}]`;
|
|
187
|
+
const nestedProxy = new DeepProxy(value, nestedPath, self.rootProxy);
|
|
188
|
+
return nestedProxy.createProxy();
|
|
189
|
+
}
|
|
190
|
+
return value;
|
|
191
|
+
},
|
|
192
|
+
set(target, prop, newValue, receiver) {
|
|
193
|
+
if (!isNaN(Number(prop))) {
|
|
194
|
+
if (!self.rootProxy) throw new Error("Root proxy is required");
|
|
195
|
+
// Capture state before change
|
|
196
|
+
const oldArray = target.slice();
|
|
197
|
+
|
|
198
|
+
const result = Reflect.set(target, prop, newValue, receiver);
|
|
199
|
+
|
|
200
|
+
// Add to history
|
|
201
|
+
self.rootProxy.history.push({
|
|
202
|
+
path: path,
|
|
203
|
+
previousValue: oldArray,
|
|
204
|
+
currentValue: target.slice(),
|
|
205
|
+
timestamp: Date.now(),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (!self.rootProxy.trackedArraysCloned.has(path)) {
|
|
209
|
+
self.rootProxy.storeArrayState(path, target);
|
|
210
|
+
}
|
|
211
|
+
self.rootProxy.notifyArrayChange(path, target);
|
|
212
|
+
self.rootProxy.notifyGlobalSubscribers();
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
return Reflect.set(target, prop, newValue, receiver);
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
subscribe(path: string, callback: Function): void {
|
|
221
|
+
if (!this.rootProxy) throw new Error("Root proxy is required");
|
|
222
|
+
|
|
223
|
+
if (path === "*") {
|
|
224
|
+
this.rootProxy.globalSubscribers.add(callback);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!this.rootProxy.subscribers.has(path)) {
|
|
229
|
+
this.rootProxy.subscribers.set(path, new Set());
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
this.rootProxy.subscribers.get(path)!.add(callback);
|
|
233
|
+
|
|
234
|
+
const actualValue = this.rootProxy.target[path];
|
|
235
|
+
const isArray = Array.isArray(actualValue);
|
|
236
|
+
|
|
237
|
+
// Fire callback immediately if there's a historical change for this path
|
|
238
|
+
const lastChange = this.rootProxy.getLastChangeForPath(path);
|
|
239
|
+
if (lastChange) {
|
|
240
|
+
if (isArray) {
|
|
241
|
+
// For arrays, we need to detect changes and fire with toCreate/toUpdate/toDelete
|
|
242
|
+
const previousArray = Array.isArray(lastChange.previousValue)
|
|
243
|
+
? lastChange.previousValue
|
|
244
|
+
: [];
|
|
245
|
+
const currentArray = Array.isArray(lastChange.currentValue)
|
|
246
|
+
? lastChange.currentValue
|
|
247
|
+
: actualValue;
|
|
248
|
+
|
|
249
|
+
// Store the initial state before the change for comparison
|
|
250
|
+
if (!this.rootProxy.trackedArraysCloned.has(path)) {
|
|
251
|
+
this.rootProxy.storeArrayState(path, previousArray);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Detect changes between previous and current
|
|
255
|
+
const changes = this.rootProxy.detectArrayChanges(
|
|
256
|
+
this.rootProxy.trackedArraysCloned.get(path) ||
|
|
257
|
+
this.rootProxy.cloneArray(previousArray),
|
|
258
|
+
previousArray,
|
|
259
|
+
currentArray
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
callback({ ...changes, path });
|
|
263
|
+
|
|
264
|
+
// Update tracked state to current
|
|
265
|
+
this.rootProxy.storeArrayState(path, currentArray);
|
|
266
|
+
} else {
|
|
267
|
+
// For non-arrays, use the simple property change format
|
|
268
|
+
callback({
|
|
269
|
+
previous: lastChange.previousValue,
|
|
270
|
+
current: lastChange.currentValue,
|
|
271
|
+
path: lastChange.path,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
} else if (isArray && !this.rootProxy.trackedArraysCloned.has(path)) {
|
|
275
|
+
// No historical changes, just store initial state
|
|
276
|
+
this.rootProxy.storeArrayState(path, actualValue);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
unsubscribe(path: string, callback: Function): void {
|
|
281
|
+
if (!this.rootProxy) throw new Error("Root proxy is required");
|
|
282
|
+
if (path === "*") {
|
|
283
|
+
this.rootProxy.globalSubscribers.delete(callback);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const subs = this.rootProxy.subscribers.get(path);
|
|
288
|
+
if (subs) {
|
|
289
|
+
subs.delete(callback);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private notifySubscribers(path: string, oldValue: any, newValue: any): void {
|
|
294
|
+
const subs = this.subscribers.get(path);
|
|
295
|
+
if (subs) {
|
|
296
|
+
subs.forEach((cb) => cb({ previous: oldValue, current: newValue, path }));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private notifyGlobalSubscribers(): void {
|
|
301
|
+
this.globalSubscribers.forEach((cb) => cb());
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private notifyArrayChange(path: string, newArray: any[]): void {
|
|
305
|
+
const subs = this.subscribers.get(path);
|
|
306
|
+
if (!subs) return;
|
|
307
|
+
|
|
308
|
+
const clonedInitial = this.trackedArraysCloned.get(path);
|
|
309
|
+
const originalInitial = this.trackedArraysOriginal.get(path);
|
|
310
|
+
|
|
311
|
+
if (!clonedInitial || !originalInitial) {
|
|
312
|
+
this.storeArrayState(path, newArray);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const changes = this.detectArrayChanges(
|
|
317
|
+
clonedInitial,
|
|
318
|
+
originalInitial,
|
|
319
|
+
newArray
|
|
320
|
+
);
|
|
321
|
+
subs.forEach((cb) => cb({ ...changes, path }));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private detectArrayChanges(
|
|
325
|
+
oldCloned: any[],
|
|
326
|
+
oldOriginal: any[],
|
|
327
|
+
newArray: any[]
|
|
328
|
+
): { toCreate: any[]; toUpdate: any[]; toDelete: any[] } {
|
|
329
|
+
const toCreate: any[] = [];
|
|
330
|
+
const toUpdate: any[] = [];
|
|
331
|
+
const toDelete: any[] = [];
|
|
332
|
+
|
|
333
|
+
const oldMap = new Map<string, any>();
|
|
334
|
+
const newMap = new Map<string, any>();
|
|
335
|
+
|
|
336
|
+
oldCloned.forEach((item) => {
|
|
337
|
+
const id = this.getItemId(item);
|
|
338
|
+
if (id) oldMap.set(id, item);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
newArray.forEach((item) => {
|
|
342
|
+
const id = this.getItemId(item);
|
|
343
|
+
if (id) newMap.set(id, item);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
newArray.forEach((item) => {
|
|
347
|
+
const id = this.getItemId(item);
|
|
348
|
+
if (!id) {
|
|
349
|
+
toCreate.push(item);
|
|
350
|
+
} else if (!oldMap.has(id)) {
|
|
351
|
+
toCreate.push(item);
|
|
352
|
+
} else if (this.hasChanged(oldMap.get(id), item)) {
|
|
353
|
+
toUpdate.push(item);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
oldOriginal.forEach((item) => {
|
|
358
|
+
const id = this.getItemId(item);
|
|
359
|
+
if (id && !newMap.has(id)) {
|
|
360
|
+
toDelete.push(item);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
return { toCreate, toUpdate, toDelete };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private cloneArray(arr: any[]): any[] {
|
|
368
|
+
return arr.map((item) => this.deepClone(item));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private deepClone(obj: any): any {
|
|
372
|
+
if (obj === null || obj === undefined) return obj;
|
|
373
|
+
if (typeof obj !== "object") return obj;
|
|
374
|
+
if (obj instanceof Date) return new Date(obj.getTime());
|
|
375
|
+
if (obj instanceof Id) return obj.value;
|
|
376
|
+
if (obj.toJson && typeof obj.toJson === "function") return obj.toJson();
|
|
377
|
+
if (Array.isArray(obj)) return obj.map((item) => this.deepClone(item));
|
|
378
|
+
const cloned: any = {};
|
|
379
|
+
for (const key in obj) {
|
|
380
|
+
if (obj.hasOwnProperty(key)) cloned[key] = this.deepClone(obj[key]);
|
|
381
|
+
}
|
|
382
|
+
return cloned;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private getItemId(item: any): string | undefined {
|
|
386
|
+
if (!item) return undefined;
|
|
387
|
+
if (item.id instanceof Id) return item.id.value;
|
|
388
|
+
if (item.id !== undefined) return String(item.id);
|
|
389
|
+
if (typeof item === "object" && "id" in item) {
|
|
390
|
+
const id = item.id;
|
|
391
|
+
return id instanceof Id ? id.value : String(id);
|
|
392
|
+
}
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private hasChanged(obj1: any, obj2: any): boolean {
|
|
397
|
+
const json1 = this.normalizeAndStringify(this.deepClone(obj1));
|
|
398
|
+
const json2 = this.normalizeAndStringify(this.deepClone(obj2));
|
|
399
|
+
return json1 !== json2;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private normalizeAndStringify(obj: any): string {
|
|
403
|
+
if (obj === null || typeof obj !== "object") {
|
|
404
|
+
return JSON.stringify(obj);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (Array.isArray(obj)) {
|
|
408
|
+
const normalizedArray = obj.map((item) =>
|
|
409
|
+
this.normalizeAndStringify(item)
|
|
410
|
+
);
|
|
411
|
+
return `[${normalizedArray.join(",")}]`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const keys = Object.keys(obj).sort();
|
|
415
|
+
const normalizedParts: string[] = [];
|
|
416
|
+
|
|
417
|
+
for (const key of keys) {
|
|
418
|
+
normalizedParts.push(`"${key}":${this.normalizeAndStringify(obj[key])}`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return `{${normalizedParts.join(",")}}`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
getHistory(): HistoryEntry[] {
|
|
425
|
+
return [...this.history];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
getLastChangeForPath(path: string): HistoryEntry | undefined {
|
|
429
|
+
for (let i = this.history.length - 1; i >= 0; i--) {
|
|
430
|
+
if (this.history[i].path === path) {
|
|
431
|
+
return this.history[i];
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
clearHistory(): void {
|
|
438
|
+
this.history = [];
|
|
439
|
+
this.originalValues.clear();
|
|
440
|
+
this.trackedArraysCloned.clear();
|
|
441
|
+
this.trackedArraysOriginal.clear();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
getTarget(): any {
|
|
445
|
+
return this.target;
|
|
446
|
+
}
|
|
447
|
+
}
|