@prestizni-software/client-dem 0.10.2 → 0.11.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/AutoUpdateClientManagerClass.d.ts +11 -0
- package/AutoUpdateClientManagerClass.js +79 -7
- package/AutoUpdateClientManagerClass.js.map +1 -1
- package/AutoUpdatedClientObjectClass.d.ts +36 -3
- package/AutoUpdatedClientObjectClass.js +520 -18
- package/AutoUpdatedClientObjectClass.js.map +1 -1
- package/CommonTypes.d.ts +173 -0
- package/CommonTypes.js.map +1 -1
- package/DEMErrors.d.ts +3 -0
- package/DEMErrors.js +7 -0
- package/DEMErrors.js.map +1 -1
- package/README.md +9 -0
- package/package.json +1 -1
- package/sync/ClientStateReconciler.d.ts +3 -1
- package/sync/ClientStateReconciler.js +46 -2
- package/sync/ClientStateReconciler.js.map +1 -1
- package/sync/types.d.ts +2 -0
- package/sync/types.js +0 -3
- package/sync/types.js.map +1 -1
- package/version.d.ts +4 -4
- package/version.js +3 -3
|
@@ -47,6 +47,157 @@ export function createReadOnlyArrayProxy(rawArray, propertyName) {
|
|
|
47
47
|
},
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Constructs a reactive DEMArray Proxy wrapper around entity array properties.
|
|
52
|
+
* Exposes synchronous queries on committed local state and asynchronous mutation Promises.
|
|
53
|
+
*
|
|
54
|
+
* @param parent - The entity object containing this array.
|
|
55
|
+
* @param propertyName - The name identifier of the array property.
|
|
56
|
+
* @param isRef - Whether this property holds cross-entity reference pointers.
|
|
57
|
+
* @param getItems - Function returning the currently resolved array items.
|
|
58
|
+
* @returns Reactive DEMArray proxy instance.
|
|
59
|
+
*/
|
|
60
|
+
export function createDEMArray(parent, propertyName, isRef, getItems) {
|
|
61
|
+
const dummyTarget = [];
|
|
62
|
+
return new Proxy(dummyTarget, {
|
|
63
|
+
get(_target, prop, receiver) {
|
|
64
|
+
if (prop === DEM_RAW_ARRAY) {
|
|
65
|
+
return parent.data?.[propertyName] ?? [];
|
|
66
|
+
}
|
|
67
|
+
if (prop === "push") {
|
|
68
|
+
return async (...items) => {
|
|
69
|
+
return parent.mutateArrayQueue(propertyName, { op: "push", items }, isRef);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (prop === "pop") {
|
|
73
|
+
return async () => {
|
|
74
|
+
return parent.mutateArrayQueue(propertyName, { op: "pop" }, isRef);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (prop === "shift") {
|
|
78
|
+
return async () => {
|
|
79
|
+
return parent.mutateArrayQueue(propertyName, { op: "shift" }, isRef);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (prop === "unshift") {
|
|
83
|
+
return async (...items) => {
|
|
84
|
+
return parent.mutateArrayQueue(propertyName, { op: "unshift", items }, isRef);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (prop === "splice") {
|
|
88
|
+
return async (start, deleteCount, ...items) => {
|
|
89
|
+
const current = getItems();
|
|
90
|
+
const actualDeleteCount = deleteCount !== undefined ? deleteCount : Math.max(0, current.length - start);
|
|
91
|
+
const targetIds = [];
|
|
92
|
+
for (let i = start; i < start + actualDeleteCount && i < current.length; i++) {
|
|
93
|
+
const el = current[i];
|
|
94
|
+
if (el && typeof el === "object" && "_id" in el) {
|
|
95
|
+
targetIds.push(String(el._id));
|
|
96
|
+
}
|
|
97
|
+
else if (el) {
|
|
98
|
+
targetIds.push(String(el));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const targetId = targetIds[0];
|
|
102
|
+
return parent.mutateArrayQueue(propertyName, {
|
|
103
|
+
op: "splice",
|
|
104
|
+
index: start,
|
|
105
|
+
deleteCount: actualDeleteCount,
|
|
106
|
+
items,
|
|
107
|
+
targetId,
|
|
108
|
+
targetIds,
|
|
109
|
+
}, isRef);
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (prop === "set") {
|
|
113
|
+
return async (index, item) => {
|
|
114
|
+
const current = getItems();
|
|
115
|
+
const targetEl = current[index];
|
|
116
|
+
const targetId = targetEl && typeof targetEl === "object" && "_id" in targetEl
|
|
117
|
+
? String(targetEl._id)
|
|
118
|
+
: targetEl
|
|
119
|
+
? String(targetEl)
|
|
120
|
+
: undefined;
|
|
121
|
+
return parent.mutateArrayQueue(propertyName, { op: "set", index, items: [item], targetId }, isRef);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (prop === "reverse") {
|
|
125
|
+
return async () => {
|
|
126
|
+
return parent.mutateArrayQueue(propertyName, { op: "reverse" }, isRef);
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (prop === "sort") {
|
|
130
|
+
return async (compareFn) => {
|
|
131
|
+
return parent.mutateArrayQueue(propertyName, { op: "sort" }, isRef);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (prop === "clear") {
|
|
135
|
+
return async () => {
|
|
136
|
+
return parent.mutateArrayQueue(propertyName, { op: "clear" }, isRef);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (prop === "ensureLoaded") {
|
|
140
|
+
return async (options) => {
|
|
141
|
+
if (!isRef)
|
|
142
|
+
return receiver;
|
|
143
|
+
const current = getItems();
|
|
144
|
+
await Promise.all(current.map(async (item) => {
|
|
145
|
+
if (item &&
|
|
146
|
+
typeof item.ensureLoaded === "function" &&
|
|
147
|
+
item.isLazyStub) {
|
|
148
|
+
await item.ensureLoaded(options);
|
|
149
|
+
}
|
|
150
|
+
}));
|
|
151
|
+
return receiver;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (MUTATING_ARRAY_METHODS.has(prop)) {
|
|
155
|
+
throw new DEMError(DEMErrorCode.CANNOT_SET_REF_DIRECTLY, `Cannot mutate DEMArray '${propertyName}' synchronously via ${String(prop)}(). Use await array.${String(prop)}(...).`);
|
|
156
|
+
}
|
|
157
|
+
const current = getItems();
|
|
158
|
+
if (prop === "length") {
|
|
159
|
+
return current.length;
|
|
160
|
+
}
|
|
161
|
+
if (typeof prop === "string" && !isNaN(Number(prop))) {
|
|
162
|
+
return current[Number(prop)];
|
|
163
|
+
}
|
|
164
|
+
const val = Reflect.get(current, prop);
|
|
165
|
+
if (typeof val === "function") {
|
|
166
|
+
return val.bind(current);
|
|
167
|
+
}
|
|
168
|
+
return val;
|
|
169
|
+
},
|
|
170
|
+
set(_target, prop) {
|
|
171
|
+
if (prop === "length" || !isNaN(Number(prop))) {
|
|
172
|
+
throw new DEMError(DEMErrorCode.CANNOT_SET_REF_DIRECTLY, `Cannot assign index directly on DEMArray '${propertyName}'. Use await array.set(index, value) or await array.splice(...).`);
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
},
|
|
176
|
+
deleteProperty(_target, prop) {
|
|
177
|
+
throw new DEMError(DEMErrorCode.CANNOT_SET_REF_DIRECTLY, `Cannot delete elements directly from DEMArray '${propertyName}'. Use await array.splice(...) or await array.clear().`);
|
|
178
|
+
},
|
|
179
|
+
has(_target, prop) {
|
|
180
|
+
const current = getItems();
|
|
181
|
+
return Reflect.has(current, prop);
|
|
182
|
+
},
|
|
183
|
+
ownKeys(_target) {
|
|
184
|
+
const current = getItems();
|
|
185
|
+
return Reflect.ownKeys(current);
|
|
186
|
+
},
|
|
187
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
188
|
+
const current = getItems();
|
|
189
|
+
if (prop === "length") {
|
|
190
|
+
_target.length = current.length;
|
|
191
|
+
return Reflect.getOwnPropertyDescriptor(_target, "length");
|
|
192
|
+
}
|
|
193
|
+
const desc = Reflect.getOwnPropertyDescriptor(current, prop);
|
|
194
|
+
if (desc) {
|
|
195
|
+
return { ...desc, configurable: true };
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
50
201
|
export class AutoUpdatedClientObject {
|
|
51
202
|
entry;
|
|
52
203
|
_refArrayCache;
|
|
@@ -85,6 +236,7 @@ export class AutoUpdatedClientObject {
|
|
|
85
236
|
}
|
|
86
237
|
}
|
|
87
238
|
EmitterID = `dem_e_${++AutoUpdatedClientObject._emitterCounter}`;
|
|
239
|
+
_preloadTimers = new Set();
|
|
88
240
|
toChangeOnParents;
|
|
89
241
|
callbacks;
|
|
90
242
|
loadReferencesAsync = async () => {
|
|
@@ -275,15 +427,21 @@ export class AutoUpdatedClientObject {
|
|
|
275
427
|
throw new DEMError(DEMErrorCode.OBJECT_LOAD_FAILED, this.loadError);
|
|
276
428
|
if (this.isLoaded)
|
|
277
429
|
return;
|
|
430
|
+
if (this.parentManager?._isClosed) {
|
|
431
|
+
throw new DEMError(DEMErrorCode.OBJECT_LOAD_FAILED, "Manager is closed");
|
|
432
|
+
}
|
|
278
433
|
await new Promise((resolve, reject) => {
|
|
279
434
|
const timer = setTimeout(() => {
|
|
435
|
+
this._preloadTimers.delete(timer);
|
|
280
436
|
const errorMsg = `Timeout waiting for object preloading (${this.className})`;
|
|
281
437
|
this.loadError = errorMsg;
|
|
282
438
|
this.isLoading = false;
|
|
283
439
|
reject(new DEMError(DEMErrorCode.PRELOAD_TIMEOUT, errorMsg));
|
|
284
440
|
}, timeoutMs);
|
|
441
|
+
this._preloadTimers.add(timer);
|
|
285
442
|
this.emitter.once(EVENT_INTERNAL_PRE_LOADED + this.EmitterID, (failed, reason) => {
|
|
286
443
|
clearTimeout(timer);
|
|
444
|
+
this._preloadTimers.delete(timer);
|
|
287
445
|
if (failed)
|
|
288
446
|
reject(new DEMError(DEMErrorCode.OBJECT_LOAD_FAILED, reason));
|
|
289
447
|
else
|
|
@@ -348,6 +506,9 @@ export class AutoUpdatedClientObject {
|
|
|
348
506
|
*/
|
|
349
507
|
get extractedData() {
|
|
350
508
|
const extracted = processIsRefProperties(this.data, this, null, [], {}, this.loggers).newData;
|
|
509
|
+
if (this.data?._id && !extracted._id) {
|
|
510
|
+
extracted._id = this.data._id?.toString?.() ?? String(this.data._id);
|
|
511
|
+
}
|
|
351
512
|
return _.cloneDeep(extracted);
|
|
352
513
|
}
|
|
353
514
|
/**
|
|
@@ -516,8 +677,16 @@ export class AutoUpdatedClientObject {
|
|
|
516
677
|
return undefined;
|
|
517
678
|
const idStr = typeof id === "string" ? id : id.toString();
|
|
518
679
|
const cached = globalCache.objects[idStr];
|
|
519
|
-
if (cached?.object)
|
|
520
|
-
|
|
680
|
+
if (cached?.object) {
|
|
681
|
+
const isSameClient = !this.parentManager ||
|
|
682
|
+
!this.parentManager.managers ||
|
|
683
|
+
Object.keys(this.parentManager.managers).length === 0 ||
|
|
684
|
+
cached.object.parentManager === this.parentManager ||
|
|
685
|
+
Object.values(this.parentManager.managers).includes(cached.object.parentManager);
|
|
686
|
+
if (isSameClient) {
|
|
687
|
+
return cached.object;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
521
690
|
const cacheKey = `${this.className}:${key}`;
|
|
522
691
|
const cachedManager = this.parentManager?.cache?.references?.[cacheKey];
|
|
523
692
|
if (cachedManager) {
|
|
@@ -571,7 +740,11 @@ export class AutoUpdatedClientObject {
|
|
|
571
740
|
key === "constructor" ||
|
|
572
741
|
key === "prototype" ||
|
|
573
742
|
(this.properties && !this.properties.includes(rootProp) && rootProp !== "_id")) {
|
|
574
|
-
return {
|
|
743
|
+
return {
|
|
744
|
+
success: false,
|
|
745
|
+
code: DEMErrorCode.INVALID_ARGUMENT,
|
|
746
|
+
msg: `Invalid property key: ${key}`,
|
|
747
|
+
};
|
|
575
748
|
}
|
|
576
749
|
try {
|
|
577
750
|
const isRef = getMetadataRecursive("isRef", this, key);
|
|
@@ -614,6 +787,7 @@ export class AutoUpdatedClientObject {
|
|
|
614
787
|
obj = obj[currentKey];
|
|
615
788
|
}
|
|
616
789
|
}
|
|
790
|
+
const prevRawVal = this.data?.[key];
|
|
617
791
|
const res = await this.setValueInternal(key, valueToStore, silent, noUpdate);
|
|
618
792
|
if (res.success) {
|
|
619
793
|
if (valueToStore === undefined) {
|
|
@@ -642,6 +816,66 @@ export class AutoUpdatedClientObject {
|
|
|
642
816
|
if (isRef && this.parentManager.isLoaded)
|
|
643
817
|
await this.contactChildren();
|
|
644
818
|
}
|
|
819
|
+
else if (isRef) {
|
|
820
|
+
const parentId = (this.data?._id ?? this._id)?.toString();
|
|
821
|
+
if (Array.isArray(valueToStore)) {
|
|
822
|
+
const nextIds = valueToStore.map((v) => v && typeof v === "object" && "_id" in v
|
|
823
|
+
? String(v._id)
|
|
824
|
+
: String(v));
|
|
825
|
+
const prevIds = Array.isArray(prevRawVal)
|
|
826
|
+
? prevRawVal.map((v) => v && typeof v === "object" && "_id" in v
|
|
827
|
+
? String(v._id)
|
|
828
|
+
: String(v))
|
|
829
|
+
: [];
|
|
830
|
+
const addedIds = nextIds.filter((id) => !prevIds.includes(id));
|
|
831
|
+
const removedIds = prevIds.filter((id) => !nextIds.includes(id));
|
|
832
|
+
for (const id of nextIds) {
|
|
833
|
+
const child = this.findReference(id, key);
|
|
834
|
+
if (child && typeof child === "object") {
|
|
835
|
+
const props = child.properties || [];
|
|
836
|
+
for (const cp of props) {
|
|
837
|
+
const refsTo = Reflect.getMetadata("refsTo", child, cp) ||
|
|
838
|
+
Reflect.getMetadata("refsTo", Object.getPrototypeOf(child), cp);
|
|
839
|
+
if (refsTo === this.className ||
|
|
840
|
+
refsTo?.startsWith?.(this.className + ":")) {
|
|
841
|
+
child[cp] = parentId;
|
|
842
|
+
if (child.data) {
|
|
843
|
+
child.data[cp] = parentId;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
for (const id of removedIds) {
|
|
850
|
+
if (nextIds.includes(id))
|
|
851
|
+
continue;
|
|
852
|
+
const child = this.findReference(id, key);
|
|
853
|
+
if (child && typeof child === "object") {
|
|
854
|
+
const props = child.properties || [];
|
|
855
|
+
for (const cp of props) {
|
|
856
|
+
const refsTo = Reflect.getMetadata("refsTo", child, cp) ||
|
|
857
|
+
Reflect.getMetadata("refsTo", Object.getPrototypeOf(child), cp);
|
|
858
|
+
if (refsTo === this.className ||
|
|
859
|
+
refsTo?.startsWith?.(this.className + ":")) {
|
|
860
|
+
const childParentVal = child[cp];
|
|
861
|
+
const childParentId = childParentVal && typeof childParentVal === "object" && "_id" in childParentVal
|
|
862
|
+
? String(childParentVal._id)
|
|
863
|
+
: String(childParentVal);
|
|
864
|
+
if (childParentId === parentId ||
|
|
865
|
+
childParentVal === parentId ||
|
|
866
|
+
childParentVal === this) {
|
|
867
|
+
child[cp] = undefined;
|
|
868
|
+
}
|
|
869
|
+
if (child.data?.[cp] === parentId ||
|
|
870
|
+
child.data?.[cp]?.toString?.() === parentId) {
|
|
871
|
+
delete child.data[cp];
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
645
879
|
if (this.isLoaded) {
|
|
646
880
|
this.callbacks.update(this, key);
|
|
647
881
|
await this.onUpdate(noUpdate, key);
|
|
@@ -667,17 +901,248 @@ export class AutoUpdatedClientObject {
|
|
|
667
901
|
async setValueInternal(key, value, silent = false, noUpdate = false) {
|
|
668
902
|
if (silent)
|
|
669
903
|
return { success: true, msg: "Silent" };
|
|
904
|
+
const id = this.data?._id ?? this._id;
|
|
905
|
+
if (!id) {
|
|
906
|
+
throw new DEMError(DEMErrorCode.MISSING_ID, `Cannot update object ${this.className} because _id is missing.`);
|
|
907
|
+
}
|
|
670
908
|
return new Promise((resolve) => {
|
|
671
|
-
const id = this.data?._id ?? this._id;
|
|
672
909
|
this.socket.emit(EVENT_UPDATE + this.className + id, { _id: id.toString(), key, value }, (res) => {
|
|
673
910
|
resolve({
|
|
674
|
-
success: res
|
|
675
|
-
code: res
|
|
676
|
-
msg: res
|
|
911
|
+
success: res?.success ?? false,
|
|
912
|
+
code: res?.code,
|
|
913
|
+
msg: res?.message ?? (res?.success ? "Success" : "Error"),
|
|
677
914
|
});
|
|
678
915
|
});
|
|
679
916
|
});
|
|
680
917
|
}
|
|
918
|
+
/**
|
|
919
|
+
* Enqueues an array mutation in the entity write queue to ensure sequential execution.
|
|
920
|
+
*
|
|
921
|
+
* @param key - Target array property key.
|
|
922
|
+
* @param req - Array mutation request payload.
|
|
923
|
+
* @param isRef - Whether the target property is a relational reference array.
|
|
924
|
+
* @returns Promise resolving to the mutation result.
|
|
925
|
+
*/
|
|
926
|
+
async mutateArrayQueue(key, req, isRef) {
|
|
927
|
+
this.writeQueue = this.writeQueue
|
|
928
|
+
.catch(() => { })
|
|
929
|
+
.then(() => this.mutateArrayInternal(key, req, isRef));
|
|
930
|
+
return this.writeQueue;
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Internal execution handler for array mutations following pessimistic convergence semantics.
|
|
934
|
+
* Transmits mutation request to server, awaits acknowledgment, and updates local state.
|
|
935
|
+
*
|
|
936
|
+
* @param key - Target array property key.
|
|
937
|
+
* @param req - Array mutation parameters payload.
|
|
938
|
+
* @param isRef - Whether the target property holds relational entities.
|
|
939
|
+
* @returns Promise resolving to the operator return value or updated array.
|
|
940
|
+
* @throws {DEMError} Throws when server rejects mutation or write fails.
|
|
941
|
+
*/
|
|
942
|
+
async mutateArrayInternal(key, req, isRef) {
|
|
943
|
+
const rawVal = this.data?.[key];
|
|
944
|
+
const currentRawArray = Array.isArray(rawVal) ? [...rawVal] : [];
|
|
945
|
+
// Capture items prior to mutation for pop/shift/splice return values
|
|
946
|
+
let removedEntities = [];
|
|
947
|
+
if (isRef) {
|
|
948
|
+
if (req.op === "pop" && currentRawArray.length > 0) {
|
|
949
|
+
const lastId = currentRawArray[currentRawArray.length - 1];
|
|
950
|
+
removedEntities = [this.findReference(lastId, key) || lastId];
|
|
951
|
+
}
|
|
952
|
+
else if (req.op === "shift" && currentRawArray.length > 0) {
|
|
953
|
+
const firstId = currentRawArray[0];
|
|
954
|
+
removedEntities = [this.findReference(firstId, key) || firstId];
|
|
955
|
+
}
|
|
956
|
+
else if (req.op === "splice" && req.index !== undefined) {
|
|
957
|
+
const count = req.deleteCount ?? currentRawArray.length - req.index;
|
|
958
|
+
for (let i = req.index; i < req.index + count && i < currentRawArray.length; i++) {
|
|
959
|
+
const id = currentRawArray[i];
|
|
960
|
+
removedEntities.push(this.findReference(id, key) || id);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
else if (req.op === "clear") {
|
|
964
|
+
for (const id of currentRawArray) {
|
|
965
|
+
removedEntities.push(this.findReference(id, key) || id);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
if (req.op === "pop" && currentRawArray.length > 0) {
|
|
971
|
+
removedEntities = [currentRawArray[currentRawArray.length - 1]];
|
|
972
|
+
}
|
|
973
|
+
else if (req.op === "shift" && currentRawArray.length > 0) {
|
|
974
|
+
removedEntities = [currentRawArray[0]];
|
|
975
|
+
}
|
|
976
|
+
else if (req.op === "splice" && req.index !== undefined) {
|
|
977
|
+
const count = req.deleteCount ?? currentRawArray.length - req.index;
|
|
978
|
+
removedEntities = currentRawArray.slice(req.index, req.index + count);
|
|
979
|
+
}
|
|
980
|
+
else if (req.op === "clear") {
|
|
981
|
+
removedEntities = [...currentRawArray];
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
// Compute baseHash
|
|
985
|
+
const baseHash = isRef
|
|
986
|
+
? currentRawArray
|
|
987
|
+
.map((item) => item && typeof item === "object" && "_id" in item
|
|
988
|
+
? String(item._id)
|
|
989
|
+
: String(item))
|
|
990
|
+
.join(",")
|
|
991
|
+
: JSON.stringify(currentRawArray);
|
|
992
|
+
// Extract raw string IDs for relational items
|
|
993
|
+
let rawItemsToPersist = req.items || [];
|
|
994
|
+
const resolvedEntities = [];
|
|
995
|
+
if (isRef && req.items) {
|
|
996
|
+
rawItemsToPersist = req.items.map((item) => {
|
|
997
|
+
const isEntityInstance = item instanceof AutoUpdatedClientObject ||
|
|
998
|
+
(item &&
|
|
999
|
+
typeof item === "object" &&
|
|
1000
|
+
"className" in item &&
|
|
1001
|
+
"data" in item);
|
|
1002
|
+
if (isEntityInstance) {
|
|
1003
|
+
resolvedEntities.push(item);
|
|
1004
|
+
return String(item._id);
|
|
1005
|
+
}
|
|
1006
|
+
const resolved = this.findReference(item, key);
|
|
1007
|
+
if (resolved) {
|
|
1008
|
+
resolvedEntities.push(resolved);
|
|
1009
|
+
}
|
|
1010
|
+
return String(item && item.toString ? item.toString() : item);
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
const payload = {
|
|
1014
|
+
...req,
|
|
1015
|
+
key,
|
|
1016
|
+
items: rawItemsToPersist,
|
|
1017
|
+
baseHash,
|
|
1018
|
+
clientTimestamp: typeof this.parentManager?.getSynchronizedTime === "function"
|
|
1019
|
+
? this.parentManager.getSynchronizedTime()
|
|
1020
|
+
: Date.now(),
|
|
1021
|
+
};
|
|
1022
|
+
let res;
|
|
1023
|
+
if (this.isServer) {
|
|
1024
|
+
res = await this.executeServerArrayMutation(key, payload, isRef);
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
const id = this.data?._id ?? this._id;
|
|
1028
|
+
if (!id) {
|
|
1029
|
+
throw new DEMError(DEMErrorCode.MISSING_ID, `Cannot mutate array on ${this.className} because _id is missing.`);
|
|
1030
|
+
}
|
|
1031
|
+
res = await new Promise((resolve) => {
|
|
1032
|
+
this.socket.emit(EVENT_UPDATE + this.className + id, { _id: id.toString(), key, mutation: payload }, (response) => {
|
|
1033
|
+
resolve(response);
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
if (!res || !res.success) {
|
|
1038
|
+
throw new DEMError(res?.code ?? DEMErrorCode.ARRAY_MUTATION_FAILED, res?.message || `Array mutation '${req.op}' on '${key}' failed on server.`);
|
|
1039
|
+
}
|
|
1040
|
+
const updatedArray = res.updatedArray ?? [];
|
|
1041
|
+
this.data[key] = updatedArray;
|
|
1042
|
+
this._refArrayCache?.delete(key);
|
|
1043
|
+
if (Object.hasOwn(this, key)) {
|
|
1044
|
+
delete this[key];
|
|
1045
|
+
}
|
|
1046
|
+
// Synchronize child back-references
|
|
1047
|
+
if (isRef) {
|
|
1048
|
+
const parentId = (this.data?._id ?? this._id)?.toString();
|
|
1049
|
+
const currentIds = updatedArray.map((v) => v && typeof v === "object" && "_id" in v
|
|
1050
|
+
? String(v._id)
|
|
1051
|
+
: String(v));
|
|
1052
|
+
for (const id of currentIds) {
|
|
1053
|
+
const child = this.findReference(id, key);
|
|
1054
|
+
if (child && typeof child === "object") {
|
|
1055
|
+
const props = child.properties || [];
|
|
1056
|
+
for (const cp of props) {
|
|
1057
|
+
const refsTo = Reflect.getMetadata("refsTo", child, cp) ||
|
|
1058
|
+
Reflect.getMetadata("refsTo", Object.getPrototypeOf(child), cp);
|
|
1059
|
+
if (refsTo === this.className ||
|
|
1060
|
+
refsTo?.startsWith?.(this.className + ":")) {
|
|
1061
|
+
child[cp] = parentId;
|
|
1062
|
+
if (child.data) {
|
|
1063
|
+
child.data[cp] = parentId;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
for (const entity of resolvedEntities) {
|
|
1070
|
+
if (entity && typeof entity === "object") {
|
|
1071
|
+
const props = entity.properties || [];
|
|
1072
|
+
for (const cp of props) {
|
|
1073
|
+
const refsTo = Reflect.getMetadata("refsTo", entity, cp) ||
|
|
1074
|
+
Reflect.getMetadata("refsTo", Object.getPrototypeOf(entity), cp);
|
|
1075
|
+
if (refsTo === this.className ||
|
|
1076
|
+
refsTo?.startsWith?.(this.className + ":")) {
|
|
1077
|
+
entity[cp] = parentId;
|
|
1078
|
+
if (entity.data) {
|
|
1079
|
+
entity.data[cp] = parentId;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (req.op === "pop" ||
|
|
1086
|
+
req.op === "shift" ||
|
|
1087
|
+
req.op === "splice" ||
|
|
1088
|
+
req.op === "clear") {
|
|
1089
|
+
for (const entity of removedEntities) {
|
|
1090
|
+
if (entity && typeof entity === "object") {
|
|
1091
|
+
const entId = entity._id?.toString() ??
|
|
1092
|
+
entity.data?._id?.toString();
|
|
1093
|
+
if (entId && currentIds.includes(entId))
|
|
1094
|
+
continue;
|
|
1095
|
+
const props = entity.properties || [];
|
|
1096
|
+
for (const cp of props) {
|
|
1097
|
+
const refsTo = Reflect.getMetadata("refsTo", entity, cp) ||
|
|
1098
|
+
Reflect.getMetadata("refsTo", Object.getPrototypeOf(entity), cp);
|
|
1099
|
+
if (refsTo === this.className ||
|
|
1100
|
+
refsTo?.startsWith?.(this.className + ":")) {
|
|
1101
|
+
const childParentVal = entity[cp];
|
|
1102
|
+
const childParentId = childParentVal &&
|
|
1103
|
+
typeof childParentVal === "object" &&
|
|
1104
|
+
"_id" in childParentVal
|
|
1105
|
+
? String(childParentVal._id)
|
|
1106
|
+
: String(childParentVal);
|
|
1107
|
+
if (childParentId === parentId ||
|
|
1108
|
+
childParentVal === parentId ||
|
|
1109
|
+
childParentVal === this) {
|
|
1110
|
+
entity[cp] = undefined;
|
|
1111
|
+
}
|
|
1112
|
+
if (entity.data?.[cp] === parentId ||
|
|
1113
|
+
entity.data?.[cp]?.toString?.() === parentId) {
|
|
1114
|
+
delete entity.data[cp];
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
if (this.isLoaded) {
|
|
1123
|
+
this.callbacks.update(this, key);
|
|
1124
|
+
await this.onUpdate(false, key);
|
|
1125
|
+
}
|
|
1126
|
+
switch (req.op) {
|
|
1127
|
+
case "push":
|
|
1128
|
+
case "unshift":
|
|
1129
|
+
return updatedArray.length;
|
|
1130
|
+
case "pop":
|
|
1131
|
+
case "shift":
|
|
1132
|
+
return removedEntities[0];
|
|
1133
|
+
case "splice":
|
|
1134
|
+
return removedEntities;
|
|
1135
|
+
case "set":
|
|
1136
|
+
return req.items?.[0];
|
|
1137
|
+
case "reverse":
|
|
1138
|
+
case "sort":
|
|
1139
|
+
return this[key];
|
|
1140
|
+
case "clear":
|
|
1141
|
+
return undefined;
|
|
1142
|
+
default:
|
|
1143
|
+
return updatedArray;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
681
1146
|
makeUpdate(key, value) {
|
|
682
1147
|
const id = this.data?._id ?? this._id;
|
|
683
1148
|
if (!id) {
|
|
@@ -714,6 +1179,11 @@ export class AutoUpdatedClientObject {
|
|
|
714
1179
|
}
|
|
715
1180
|
async wipeSelf() {
|
|
716
1181
|
this._refArrayCache?.clear();
|
|
1182
|
+
for (const timer of this._preloadTimers) {
|
|
1183
|
+
clearTimeout(timer);
|
|
1184
|
+
}
|
|
1185
|
+
this._preloadTimers.clear();
|
|
1186
|
+
this.emitter.emit(EVENT_INTERNAL_PRE_LOADED + this.EmitterID, true, "Object destroyed");
|
|
717
1187
|
if (this.data.Wiped)
|
|
718
1188
|
return;
|
|
719
1189
|
const id = this.data?._id ?? this._id;
|
|
@@ -1003,7 +1473,8 @@ export function setupClassAccessors(classParam) {
|
|
|
1003
1473
|
}
|
|
1004
1474
|
const cached = this._refArrayCache.get(key);
|
|
1005
1475
|
if (cached && cached.rawIdsHash === idsHash) {
|
|
1006
|
-
const
|
|
1476
|
+
const childrenToCheck = cached.resolvedChildren || cached.proxy;
|
|
1477
|
+
const hasStaleChild = childrenToCheck.some((child) => {
|
|
1007
1478
|
if (!child)
|
|
1008
1479
|
return true;
|
|
1009
1480
|
if (child.data?.Wiped)
|
|
@@ -1020,23 +1491,54 @@ export function setupClassAccessors(classParam) {
|
|
|
1020
1491
|
}
|
|
1021
1492
|
this._refArrayCache.delete(key);
|
|
1022
1493
|
}
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1494
|
+
const getResolvedItems = () => {
|
|
1495
|
+
const rawList = this.data?.[key];
|
|
1496
|
+
if (!Array.isArray(rawList))
|
|
1497
|
+
return [];
|
|
1498
|
+
const rawResult = [];
|
|
1499
|
+
for (let i = 0; i < rawList.length; i++) {
|
|
1500
|
+
const item = rawList[i];
|
|
1501
|
+
if (item) {
|
|
1502
|
+
const ref = this.findReference(item, key);
|
|
1503
|
+
if (ref &&
|
|
1504
|
+
!ref.data?.Wiped &&
|
|
1505
|
+
(!ref._id || globalCache.objects[ref._id.toString()])) {
|
|
1506
|
+
rawResult.push(ref);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1030
1509
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1510
|
+
return rawResult;
|
|
1511
|
+
};
|
|
1512
|
+
const resolvedChildren = getResolvedItems();
|
|
1513
|
+
const proxy = createDEMArray(this, key, true, getResolvedItems);
|
|
1514
|
+
this._refArrayCache.set(key, {
|
|
1515
|
+
rawIdsHash: idsHash,
|
|
1516
|
+
proxy: proxy,
|
|
1517
|
+
resolvedChildren,
|
|
1518
|
+
});
|
|
1034
1519
|
return proxy;
|
|
1035
1520
|
}
|
|
1036
1521
|
else {
|
|
1037
1522
|
return this.findReference(val, key);
|
|
1038
1523
|
}
|
|
1039
1524
|
}
|
|
1525
|
+
if (Array.isArray(val)) {
|
|
1526
|
+
const idsHash = JSON.stringify(val);
|
|
1527
|
+
if (!this._refArrayCache) {
|
|
1528
|
+
this._refArrayCache = new Map();
|
|
1529
|
+
}
|
|
1530
|
+
const cached = this._refArrayCache.get(key);
|
|
1531
|
+
if (cached && cached.rawIdsHash === idsHash) {
|
|
1532
|
+
return cached.proxy;
|
|
1533
|
+
}
|
|
1534
|
+
const getPrimitiveItems = () => {
|
|
1535
|
+
const raw = this.data?.[key];
|
|
1536
|
+
return Array.isArray(raw) ? raw : [];
|
|
1537
|
+
};
|
|
1538
|
+
const proxy = createDEMArray(this, key, false, getPrimitiveItems);
|
|
1539
|
+
this._refArrayCache.set(key, { rawIdsHash: idsHash, proxy: proxy });
|
|
1540
|
+
return proxy;
|
|
1541
|
+
}
|
|
1040
1542
|
return val;
|
|
1041
1543
|
},
|
|
1042
1544
|
set(v) {
|