@prestizni-software/server-dem 0.10.3 → 0.11.1

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.
@@ -1,5 +1,5 @@
1
1
  import "reflect-metadata";
2
- import { Constructor, EventEmitter3, IsData, LoggersType, PathValueOf, Paths, ServerUpdateRequest, ExtractedData, MongoId, IAutoUpdatedClientObject, IAutoUpdatedClientObjectBase, IAutoUpdateManager, DEMCache, DEMErrorCode } from "./CommonTypes.js";
2
+ import { Constructor, EventEmitter3, IsData, LoggersType, PathValueOf, Paths, ServerUpdateRequest, ExtractedData, MongoId, IAutoUpdatedClientObject, IAutoUpdatedClientObjectBase, IAutoUpdateManager, DEMCache, DEMErrorCode, DEMArray, DEMArrayMutationParams } from "./CommonTypes.js";
3
3
  import { ObjectId } from "bson";
4
4
  import { Socket } from "socket.io-client";
5
5
  export type DEMClientCallbacks<T extends object> = {
@@ -18,12 +18,24 @@ export declare const DEM_RAW_ARRAY: unique symbol;
18
18
  * @returns Immutable proxy wrapping the underlying entity array.
19
19
  */
20
20
  export declare function createReadOnlyArrayProxy<T>(rawArray: T[], propertyName: string): T[];
21
+ /**
22
+ * Constructs a reactive DEMArray Proxy wrapper around entity array properties.
23
+ * Exposes synchronous queries on committed local state and asynchronous mutation Promises.
24
+ *
25
+ * @param parent - The entity object containing this array.
26
+ * @param propertyName - The name identifier of the array property.
27
+ * @param isRef - Whether this property holds cross-entity reference pointers.
28
+ * @param getItems - Function returning the currently resolved array items.
29
+ * @returns Reactive DEMArray proxy instance.
30
+ */
31
+ export declare function createDEMArray<T>(parent: AutoUpdatedClientObject<any>, propertyName: string, isRef: boolean, getItems: () => any[]): DEMArray<T>;
21
32
  type SocketType = Socket;
22
33
  export declare abstract class AutoUpdatedClientObject<T extends IAutoUpdatedClientObjectBase, M extends Record<string, IAutoUpdateManager<any>> = Record<string, IAutoUpdateManager<any>>> implements IAutoUpdatedClientObject<T> {
23
34
  protected entry: unknown;
24
35
  _refArrayCache?: Map<string, {
25
36
  rawIdsHash: string;
26
37
  proxy: any[];
38
+ resolvedChildren?: any[];
27
39
  }>;
28
40
  protected readonly socket: SocketType;
29
41
  protected data: IsData<T>;
@@ -57,11 +69,13 @@ export declare abstract class AutoUpdatedClientObject<T extends IAutoUpdatedClie
57
69
  */
58
70
  static getDescriptor(data: Partial<any>): string;
59
71
  protected readonly EmitterID: string;
72
+ protected _preloadTimers: Set<ReturnType<typeof setTimeout>>;
60
73
  protected toChangeOnParents?: {
61
74
  key: string;
62
75
  value: unknown;
63
76
  }[];
64
77
  callbacks: DEMClientCallbacks<T>;
78
+ protected _isUpdating: boolean;
65
79
  private readonly loadReferencesAsync;
66
80
  /**
67
81
  * Initializes an AutoUpdatedClientObject instance.
@@ -199,6 +213,26 @@ export declare abstract class AutoUpdatedClientObject<T extends IAutoUpdatedClie
199
213
  msg: string;
200
214
  code?: DEMErrorCode;
201
215
  }>;
216
+ /**
217
+ * Enqueues an array mutation in the entity write queue to ensure sequential execution.
218
+ *
219
+ * @param key - Target array property key.
220
+ * @param req - Array mutation request payload.
221
+ * @param isRef - Whether the target property is a relational reference array.
222
+ * @returns Promise resolving to the mutation result.
223
+ */
224
+ mutateArrayQueue(key: string, req: DEMArrayMutationParams, isRef: boolean): Promise<any>;
225
+ /**
226
+ * Internal execution handler for array mutations following pessimistic convergence semantics.
227
+ * Transmits mutation request to server, awaits acknowledgment, and updates local state.
228
+ *
229
+ * @param key - Target array property key.
230
+ * @param req - Array mutation parameters payload.
231
+ * @param isRef - Whether the target property holds relational entities.
232
+ * @returns Promise resolving to the operator return value or updated array.
233
+ * @throws {DEMError} Throws when server rejects mutation or write fails.
234
+ */
235
+ private mutateArrayInternal;
202
236
  protected makeUpdate(key: string, value: unknown): ServerUpdateRequest<T>;
203
237
  protected resolveReference(id: string): Promise<IAutoUpdatedClientObject<any> | null>;
204
238
  private findAndLoadReferences;
@@ -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,8 +236,10 @@ export class AutoUpdatedClientObject {
85
236
  }
86
237
  }
87
238
  EmitterID = `dem_e_${++AutoUpdatedClientObject._emitterCounter}`;
239
+ _preloadTimers = new Set();
88
240
  toChangeOnParents;
89
241
  callbacks;
242
+ _isUpdating = false;
90
243
  loadReferencesAsync = async () => {
91
244
  try {
92
245
  if (!this.isLoaded) {
@@ -275,15 +428,21 @@ export class AutoUpdatedClientObject {
275
428
  throw new DEMError(DEMErrorCode.OBJECT_LOAD_FAILED, this.loadError);
276
429
  if (this.isLoaded)
277
430
  return;
431
+ if (this.parentManager?._isClosed) {
432
+ throw new DEMError(DEMErrorCode.OBJECT_LOAD_FAILED, "Manager is closed");
433
+ }
278
434
  await new Promise((resolve, reject) => {
279
435
  const timer = setTimeout(() => {
436
+ this._preloadTimers.delete(timer);
280
437
  const errorMsg = `Timeout waiting for object preloading (${this.className})`;
281
438
  this.loadError = errorMsg;
282
439
  this.isLoading = false;
283
440
  reject(new DEMError(DEMErrorCode.PRELOAD_TIMEOUT, errorMsg));
284
441
  }, timeoutMs);
442
+ this._preloadTimers.add(timer);
285
443
  this.emitter.once(EVENT_INTERNAL_PRE_LOADED + this.EmitterID, (failed, reason) => {
286
444
  clearTimeout(timer);
445
+ this._preloadTimers.delete(timer);
287
446
  if (failed)
288
447
  reject(new DEMError(DEMErrorCode.OBJECT_LOAD_FAILED, reason));
289
448
  else
@@ -519,8 +678,16 @@ export class AutoUpdatedClientObject {
519
678
  return undefined;
520
679
  const idStr = typeof id === "string" ? id : id.toString();
521
680
  const cached = globalCache.objects[idStr];
522
- if (cached?.object)
523
- return cached.object;
681
+ if (cached?.object) {
682
+ const isSameClient = !this.parentManager ||
683
+ !this.parentManager.managers ||
684
+ Object.keys(this.parentManager.managers).length === 0 ||
685
+ cached.object.parentManager === this.parentManager ||
686
+ Object.values(this.parentManager.managers).includes(cached.object.parentManager);
687
+ if (isSameClient) {
688
+ return cached.object;
689
+ }
690
+ }
524
691
  const cacheKey = `${this.className}:${key}`;
525
692
  const cachedManager = this.parentManager?.cache?.references?.[cacheKey];
526
693
  if (cachedManager) {
@@ -621,6 +788,7 @@ export class AutoUpdatedClientObject {
621
788
  obj = obj[currentKey];
622
789
  }
623
790
  }
791
+ const prevRawVal = this.data?.[key];
624
792
  const res = await this.setValueInternal(key, valueToStore, silent, noUpdate);
625
793
  if (res.success) {
626
794
  if (valueToStore === undefined) {
@@ -649,6 +817,66 @@ export class AutoUpdatedClientObject {
649
817
  if (isRef && this.parentManager.isLoaded)
650
818
  await this.contactChildren();
651
819
  }
820
+ else if (isRef) {
821
+ const parentId = (this.data?._id ?? this._id)?.toString();
822
+ if (Array.isArray(valueToStore)) {
823
+ const nextIds = valueToStore.map((v) => v && typeof v === "object" && "_id" in v
824
+ ? String(v._id)
825
+ : String(v));
826
+ const prevIds = Array.isArray(prevRawVal)
827
+ ? prevRawVal.map((v) => v && typeof v === "object" && "_id" in v
828
+ ? String(v._id)
829
+ : String(v))
830
+ : [];
831
+ const addedIds = nextIds.filter((id) => !prevIds.includes(id));
832
+ const removedIds = prevIds.filter((id) => !nextIds.includes(id));
833
+ for (const id of nextIds) {
834
+ const child = this.findReference(id, key);
835
+ if (child && typeof child === "object") {
836
+ const props = child.properties || [];
837
+ for (const cp of props) {
838
+ const refsTo = Reflect.getMetadata("refsTo", child, cp) ||
839
+ Reflect.getMetadata("refsTo", Object.getPrototypeOf(child), cp);
840
+ if (refsTo === this.className ||
841
+ refsTo?.startsWith?.(this.className + ":")) {
842
+ child[cp] = parentId;
843
+ if (child.data) {
844
+ child.data[cp] = parentId;
845
+ }
846
+ }
847
+ }
848
+ }
849
+ }
850
+ for (const id of removedIds) {
851
+ if (nextIds.includes(id))
852
+ continue;
853
+ const child = this.findReference(id, key);
854
+ if (child && typeof child === "object") {
855
+ const props = child.properties || [];
856
+ for (const cp of props) {
857
+ const refsTo = Reflect.getMetadata("refsTo", child, cp) ||
858
+ Reflect.getMetadata("refsTo", Object.getPrototypeOf(child), cp);
859
+ if (refsTo === this.className ||
860
+ refsTo?.startsWith?.(this.className + ":")) {
861
+ const childParentVal = child[cp];
862
+ const childParentId = childParentVal && typeof childParentVal === "object" && "_id" in childParentVal
863
+ ? String(childParentVal._id)
864
+ : String(childParentVal);
865
+ if (childParentId === parentId ||
866
+ childParentVal === parentId ||
867
+ childParentVal === this) {
868
+ child[cp] = undefined;
869
+ }
870
+ if (child.data?.[cp] === parentId ||
871
+ child.data?.[cp]?.toString?.() === parentId) {
872
+ delete child.data[cp];
873
+ }
874
+ }
875
+ }
876
+ }
877
+ }
878
+ }
879
+ }
652
880
  if (this.isLoaded) {
653
881
  this.callbacks.update(this, key);
654
882
  await this.onUpdate(noUpdate, key);
@@ -688,6 +916,237 @@ export class AutoUpdatedClientObject {
688
916
  });
689
917
  });
690
918
  }
919
+ /**
920
+ * Enqueues an array mutation in the entity write queue to ensure sequential execution.
921
+ *
922
+ * @param key - Target array property key.
923
+ * @param req - Array mutation request payload.
924
+ * @param isRef - Whether the target property is a relational reference array.
925
+ * @returns Promise resolving to the mutation result.
926
+ */
927
+ async mutateArrayQueue(key, req, isRef) {
928
+ if (this._isUpdating) {
929
+ return this.mutateArrayInternal(key, req, isRef);
930
+ }
931
+ this.writeQueue = this.writeQueue
932
+ .catch(() => { })
933
+ .then(() => this.mutateArrayInternal(key, req, isRef));
934
+ return this.writeQueue;
935
+ }
936
+ /**
937
+ * Internal execution handler for array mutations following pessimistic convergence semantics.
938
+ * Transmits mutation request to server, awaits acknowledgment, and updates local state.
939
+ *
940
+ * @param key - Target array property key.
941
+ * @param req - Array mutation parameters payload.
942
+ * @param isRef - Whether the target property holds relational entities.
943
+ * @returns Promise resolving to the operator return value or updated array.
944
+ * @throws {DEMError} Throws when server rejects mutation or write fails.
945
+ */
946
+ async mutateArrayInternal(key, req, isRef) {
947
+ const rawVal = this.data?.[key];
948
+ const currentRawArray = Array.isArray(rawVal) ? [...rawVal] : [];
949
+ // Capture items prior to mutation for pop/shift/splice return values
950
+ let removedEntities = [];
951
+ if (isRef) {
952
+ if (req.op === "pop" && currentRawArray.length > 0) {
953
+ const lastId = currentRawArray[currentRawArray.length - 1];
954
+ removedEntities = [this.findReference(lastId, key) || lastId];
955
+ }
956
+ else if (req.op === "shift" && currentRawArray.length > 0) {
957
+ const firstId = currentRawArray[0];
958
+ removedEntities = [this.findReference(firstId, key) || firstId];
959
+ }
960
+ else if (req.op === "splice" && req.index !== undefined) {
961
+ const count = req.deleteCount ?? currentRawArray.length - req.index;
962
+ for (let i = req.index; i < req.index + count && i < currentRawArray.length; i++) {
963
+ const id = currentRawArray[i];
964
+ removedEntities.push(this.findReference(id, key) || id);
965
+ }
966
+ }
967
+ else if (req.op === "clear") {
968
+ for (const id of currentRawArray) {
969
+ removedEntities.push(this.findReference(id, key) || id);
970
+ }
971
+ }
972
+ }
973
+ else {
974
+ if (req.op === "pop" && currentRawArray.length > 0) {
975
+ removedEntities = [currentRawArray[currentRawArray.length - 1]];
976
+ }
977
+ else if (req.op === "shift" && currentRawArray.length > 0) {
978
+ removedEntities = [currentRawArray[0]];
979
+ }
980
+ else if (req.op === "splice" && req.index !== undefined) {
981
+ const count = req.deleteCount ?? currentRawArray.length - req.index;
982
+ removedEntities = currentRawArray.slice(req.index, req.index + count);
983
+ }
984
+ else if (req.op === "clear") {
985
+ removedEntities = [...currentRawArray];
986
+ }
987
+ }
988
+ // Compute baseHash
989
+ const baseHash = isRef
990
+ ? currentRawArray
991
+ .map((item) => item && typeof item === "object" && "_id" in item
992
+ ? String(item._id)
993
+ : String(item))
994
+ .join(",")
995
+ : JSON.stringify(currentRawArray);
996
+ // Extract raw string IDs for relational items
997
+ let rawItemsToPersist = req.items || [];
998
+ const resolvedEntities = [];
999
+ if (isRef && req.items) {
1000
+ rawItemsToPersist = req.items.map((item) => {
1001
+ const isEntityInstance = item instanceof AutoUpdatedClientObject ||
1002
+ (item &&
1003
+ typeof item === "object" &&
1004
+ "className" in item &&
1005
+ "data" in item);
1006
+ if (isEntityInstance) {
1007
+ resolvedEntities.push(item);
1008
+ return String(item._id);
1009
+ }
1010
+ const resolved = this.findReference(item, key);
1011
+ if (resolved) {
1012
+ resolvedEntities.push(resolved);
1013
+ }
1014
+ return String(item && item.toString ? item.toString() : item);
1015
+ });
1016
+ }
1017
+ const payload = {
1018
+ ...req,
1019
+ key,
1020
+ items: rawItemsToPersist,
1021
+ baseHash,
1022
+ clientTimestamp: typeof this.parentManager?.getSynchronizedTime === "function"
1023
+ ? this.parentManager.getSynchronizedTime()
1024
+ : Date.now(),
1025
+ };
1026
+ let res;
1027
+ if (this.isServer) {
1028
+ res = await this.executeServerArrayMutation(key, payload, isRef);
1029
+ }
1030
+ else {
1031
+ const id = this.data?._id ?? this._id;
1032
+ if (!id) {
1033
+ throw new DEMError(DEMErrorCode.MISSING_ID, `Cannot mutate array on ${this.className} because _id is missing.`);
1034
+ }
1035
+ res = await new Promise((resolve) => {
1036
+ this.socket.emit(EVENT_UPDATE + this.className + id, { _id: id.toString(), key, mutation: payload }, (response) => {
1037
+ resolve(response);
1038
+ });
1039
+ });
1040
+ }
1041
+ if (!res || !res.success) {
1042
+ throw new DEMError(res?.code ?? DEMErrorCode.ARRAY_MUTATION_FAILED, res?.message || `Array mutation '${req.op}' on '${key}' failed on server.`);
1043
+ }
1044
+ const updatedArray = res.updatedArray ?? [];
1045
+ this.data[key] = updatedArray;
1046
+ this._refArrayCache?.delete(key);
1047
+ if (Object.hasOwn(this, key)) {
1048
+ delete this[key];
1049
+ }
1050
+ // Synchronize child back-references
1051
+ if (isRef) {
1052
+ const parentId = (this.data?._id ?? this._id)?.toString();
1053
+ const currentIds = updatedArray.map((v) => v && typeof v === "object" && "_id" in v
1054
+ ? String(v._id)
1055
+ : String(v));
1056
+ for (const id of currentIds) {
1057
+ const child = this.findReference(id, key);
1058
+ if (child && typeof child === "object") {
1059
+ const props = child.properties || [];
1060
+ for (const cp of props) {
1061
+ const refsTo = Reflect.getMetadata("refsTo", child, cp) ||
1062
+ Reflect.getMetadata("refsTo", Object.getPrototypeOf(child), cp);
1063
+ if (refsTo === this.className ||
1064
+ refsTo?.startsWith?.(this.className + ":")) {
1065
+ child[cp] = parentId;
1066
+ if (child.data) {
1067
+ child.data[cp] = parentId;
1068
+ }
1069
+ }
1070
+ }
1071
+ }
1072
+ }
1073
+ for (const entity of resolvedEntities) {
1074
+ if (entity && typeof entity === "object") {
1075
+ const props = entity.properties || [];
1076
+ for (const cp of props) {
1077
+ const refsTo = Reflect.getMetadata("refsTo", entity, cp) ||
1078
+ Reflect.getMetadata("refsTo", Object.getPrototypeOf(entity), cp);
1079
+ if (refsTo === this.className ||
1080
+ refsTo?.startsWith?.(this.className + ":")) {
1081
+ entity[cp] = parentId;
1082
+ if (entity.data) {
1083
+ entity.data[cp] = parentId;
1084
+ }
1085
+ }
1086
+ }
1087
+ }
1088
+ }
1089
+ if (req.op === "pop" ||
1090
+ req.op === "shift" ||
1091
+ req.op === "splice" ||
1092
+ req.op === "clear") {
1093
+ for (const entity of removedEntities) {
1094
+ if (entity && typeof entity === "object") {
1095
+ const entId = entity._id?.toString() ??
1096
+ entity.data?._id?.toString();
1097
+ if (entId && currentIds.includes(entId))
1098
+ continue;
1099
+ const props = entity.properties || [];
1100
+ for (const cp of props) {
1101
+ const refsTo = Reflect.getMetadata("refsTo", entity, cp) ||
1102
+ Reflect.getMetadata("refsTo", Object.getPrototypeOf(entity), cp);
1103
+ if (refsTo === this.className ||
1104
+ refsTo?.startsWith?.(this.className + ":")) {
1105
+ const childParentVal = entity[cp];
1106
+ const childParentId = childParentVal &&
1107
+ typeof childParentVal === "object" &&
1108
+ "_id" in childParentVal
1109
+ ? String(childParentVal._id)
1110
+ : String(childParentVal);
1111
+ if (childParentId === parentId ||
1112
+ childParentVal === parentId ||
1113
+ childParentVal === this) {
1114
+ entity[cp] = undefined;
1115
+ }
1116
+ if (entity.data?.[cp] === parentId ||
1117
+ entity.data?.[cp]?.toString?.() === parentId) {
1118
+ delete entity.data[cp];
1119
+ }
1120
+ }
1121
+ }
1122
+ }
1123
+ }
1124
+ }
1125
+ }
1126
+ if (this.isLoaded) {
1127
+ this.callbacks.update(this, key);
1128
+ await this.onUpdate(false, key);
1129
+ }
1130
+ switch (req.op) {
1131
+ case "push":
1132
+ case "unshift":
1133
+ return updatedArray.length;
1134
+ case "pop":
1135
+ case "shift":
1136
+ return removedEntities[0];
1137
+ case "splice":
1138
+ return removedEntities;
1139
+ case "set":
1140
+ return req.items?.[0];
1141
+ case "reverse":
1142
+ case "sort":
1143
+ return this[key];
1144
+ case "clear":
1145
+ return undefined;
1146
+ default:
1147
+ return updatedArray;
1148
+ }
1149
+ }
691
1150
  makeUpdate(key, value) {
692
1151
  const id = this.data?._id ?? this._id;
693
1152
  if (!id) {
@@ -724,6 +1183,11 @@ export class AutoUpdatedClientObject {
724
1183
  }
725
1184
  async wipeSelf() {
726
1185
  this._refArrayCache?.clear();
1186
+ for (const timer of this._preloadTimers) {
1187
+ clearTimeout(timer);
1188
+ }
1189
+ this._preloadTimers.clear();
1190
+ this.emitter.emit(EVENT_INTERNAL_PRE_LOADED + this.EmitterID, true, "Object destroyed");
727
1191
  if (this.data.Wiped)
728
1192
  return;
729
1193
  const id = this.data?._id ?? this._id;
@@ -1013,7 +1477,8 @@ export function setupClassAccessors(classParam) {
1013
1477
  }
1014
1478
  const cached = this._refArrayCache.get(key);
1015
1479
  if (cached && cached.rawIdsHash === idsHash) {
1016
- const hasStaleChild = cached.proxy.some((child) => {
1480
+ const childrenToCheck = cached.resolvedChildren || cached.proxy;
1481
+ const hasStaleChild = childrenToCheck.some((child) => {
1017
1482
  if (!child)
1018
1483
  return true;
1019
1484
  if (child.data?.Wiped)
@@ -1030,23 +1495,54 @@ export function setupClassAccessors(classParam) {
1030
1495
  }
1031
1496
  this._refArrayCache.delete(key);
1032
1497
  }
1033
- const rawResult = [];
1034
- for (let i = 0; i < val.length; i++) {
1035
- const item = val[i];
1036
- if (item) {
1037
- const ref = this.findReference(item, key);
1038
- if (ref)
1039
- rawResult.push(ref);
1498
+ const getResolvedItems = () => {
1499
+ const rawList = this.data?.[key];
1500
+ if (!Array.isArray(rawList))
1501
+ return [];
1502
+ const rawResult = [];
1503
+ for (let i = 0; i < rawList.length; i++) {
1504
+ const item = rawList[i];
1505
+ if (item) {
1506
+ const ref = this.findReference(item, key);
1507
+ if (ref &&
1508
+ !ref.data?.Wiped &&
1509
+ (!ref._id || globalCache.objects[ref._id.toString()])) {
1510
+ rawResult.push(ref);
1511
+ }
1512
+ }
1040
1513
  }
1041
- }
1042
- const proxy = createReadOnlyArrayProxy(rawResult, key);
1043
- this._refArrayCache.set(key, { rawIdsHash: idsHash, proxy });
1514
+ return rawResult;
1515
+ };
1516
+ const resolvedChildren = getResolvedItems();
1517
+ const proxy = createDEMArray(this, key, true, getResolvedItems);
1518
+ this._refArrayCache.set(key, {
1519
+ rawIdsHash: idsHash,
1520
+ proxy: proxy,
1521
+ resolvedChildren,
1522
+ });
1044
1523
  return proxy;
1045
1524
  }
1046
1525
  else {
1047
1526
  return this.findReference(val, key);
1048
1527
  }
1049
1528
  }
1529
+ if (Array.isArray(val)) {
1530
+ const idsHash = JSON.stringify(val);
1531
+ if (!this._refArrayCache) {
1532
+ this._refArrayCache = new Map();
1533
+ }
1534
+ const cached = this._refArrayCache.get(key);
1535
+ if (cached && cached.rawIdsHash === idsHash) {
1536
+ return cached.proxy;
1537
+ }
1538
+ const getPrimitiveItems = () => {
1539
+ const raw = this.data?.[key];
1540
+ return Array.isArray(raw) ? raw : [];
1541
+ };
1542
+ const proxy = createDEMArray(this, key, false, getPrimitiveItems);
1543
+ this._refArrayCache.set(key, { rawIdsHash: idsHash, proxy: proxy });
1544
+ return proxy;
1545
+ }
1050
1546
  return val;
1051
1547
  },
1052
1548
  set(v) {