@stonyx/orm 0.3.2-alpha.14 → 0.3.2-alpha.15

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.
@@ -52,34 +52,13 @@ export function createRecord(modelName, rawData = {}, userOptions = {}) {
52
52
  relationship.push(record);
53
53
  pendingHasMany.splice(0);
54
54
  }
55
- // FK-based inverse hasMany wiring — when a child record is created with a
56
- // foreign-key field (e.g. `owner: 'owner-1'` on an animal), find any parent
57
- // whose hasMany registry targets this model and push the child into the
58
- // parent's shared array. This covers edge cases where the child is created
59
- // in a separate async frame without a belongsTo handler firing.
60
- const hasManyReg = getHasManyRegistry();
61
- if (hasManyReg) {
62
- for (const [parentModelName, targetMap] of hasManyReg) {
63
- const childArrayMap = targetMap.get(modelName);
64
- if (!childArrayMap)
65
- continue;
66
- // Check if rawData contains a FK field matching the parent model name
67
- const fkValue = rawData[parentModelName];
68
- if (fkValue === undefined || fkValue === null)
69
- continue;
70
- const parentArray = childArrayMap.get(fkValue);
71
- if (parentArray && !parentArray.includes(record)) {
72
- parentArray.push(record);
73
- }
74
- }
75
- }
76
55
  // Fulfill pending belongsTo relationships
77
56
  const pendingBelongsToQueue = getPendingBelongsToRegistry();
78
57
  const pendingBelongsToRaw = pendingBelongsToQueue.get(modelName)?.get(record.id);
79
58
  const pendingBelongsTo = Array.isArray(pendingBelongsToRaw) ? pendingBelongsToRaw : undefined;
80
59
  if (pendingBelongsTo) {
81
60
  const belongsToReg = getBelongsToRegistry();
82
- const pendingHasManyReg = getHasManyRegistry();
61
+ const hasManyReg = getHasManyRegistry();
83
62
  for (const { sourceRecord, sourceModelName, relationshipKey, relationshipId } of pendingBelongsTo) {
84
63
  // Update the belongsTo relationship on the source record
85
64
  sourceRecord.__relationships[relationshipKey] = record;
@@ -93,7 +72,7 @@ export function createRecord(modelName, rawData = {}, userOptions = {}) {
93
72
  }
94
73
  }
95
74
  // Wire inverse hasMany if it exists
96
- const inverseHasMany = pendingHasManyReg.get(modelName)?.get(sourceModelName)?.get(record.id);
75
+ const inverseHasMany = hasManyReg.get(modelName)?.get(sourceModelName)?.get(record.id);
97
76
  if (inverseHasMany && !inverseHasMany.includes(sourceRecord)) {
98
77
  inverseHasMany.push(sourceRecord);
99
78
  }
@@ -105,13 +84,22 @@ export function createRecord(modelName, rawData = {}, userOptions = {}) {
105
84
  const shouldPersist = orm?.sqlDb && !options.isDbRecord && !userOptions._relationshipKey && !options._skipAutoPersist;
106
85
  if (shouldPersist) {
107
86
  const response = { data: { id: record.id } };
108
- orm.sqlDb.persist('create', modelName, { rawData }, response).catch((err) => {
87
+ orm.sqlDb.persist('create', modelName, { rawData }, response)
88
+ .catch((err) => {
109
89
  orm.emitPersistError({
110
90
  operation: 'create',
111
91
  modelName,
112
92
  recordId: record.id,
113
93
  error: err instanceof Error ? err : new Error(String(err)),
114
94
  });
95
+ })
96
+ .finally(() => {
97
+ // Evict non-memory records after persist to prevent unbounded heap growth (stonyx#81)
98
+ if (store._memoryResolver && !store._memoryResolver(modelName)) {
99
+ const ms = store.get(modelName);
100
+ if (ms)
101
+ ms.delete(record.id);
102
+ }
115
103
  });
116
104
  }
117
105
  return record;
@@ -79,33 +79,8 @@ export default class Serializer {
79
79
  // Pass relationship key name to handler for pending fulfillment
80
80
  const handlerOptions = { ...options, _relationshipKey: key };
81
81
  const childRecord = handler(record, data, handlerOptions);
82
- // hasMany relationships use a getter so format()/toJSON() always read
83
- // the live registry array instead of a stale snapshot captured at
84
- // serialization time. This is critical when child records are created
85
- // in a later async frame — the belongsTo inverse wiring pushes into
86
- // the shared registry array, and the getter ensures the parent sees it.
87
- const isHasMany = handler.__relationshipType === 'hasMany';
88
- if (isHasMany) {
89
- // `childRecord` IS the shared registry array — define a getter that
90
- // always dereferences through the same array reference.
91
- const registryArray = childRecord;
92
- Object.defineProperty(rec, key, {
93
- enumerable: true,
94
- configurable: true,
95
- get: () => registryArray,
96
- set(v) { relatedRecords[key] = v; }
97
- });
98
- Object.defineProperty(relatedRecords, key, {
99
- enumerable: true,
100
- configurable: true,
101
- get: () => registryArray,
102
- set(v) { Object.defineProperty(relatedRecords, key, { value: v, writable: true, enumerable: true, configurable: true }); }
103
- });
104
- }
105
- else {
106
- rec[key] = childRecord;
107
- relatedRecords[key] = childRecord;
108
- }
82
+ rec[key] = childRecord;
83
+ relatedRecords[key] = childRecord;
109
84
  continue;
110
85
  }
111
86
  // Aggregate property handling — use the rawData value, not the aggregate descriptor
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.3.2-alpha.14",
7
+ "version": "0.3.2-alpha.15",
8
8
  "description": "",
9
9
  "main": "dist/index.js",
10
10
  "type": "module",
@@ -79,28 +79,6 @@ export function createRecord(modelName: string, rawData: { [key: string]: unknow
79
79
  pendingHasMany.splice(0);
80
80
  }
81
81
 
82
- // FK-based inverse hasMany wiring — when a child record is created with a
83
- // foreign-key field (e.g. `owner: 'owner-1'` on an animal), find any parent
84
- // whose hasMany registry targets this model and push the child into the
85
- // parent's shared array. This covers edge cases where the child is created
86
- // in a separate async frame without a belongsTo handler firing.
87
- const hasManyReg = getHasManyRegistry();
88
- if (hasManyReg) {
89
- for (const [parentModelName, targetMap] of hasManyReg) {
90
- const childArrayMap = targetMap.get(modelName);
91
- if (!childArrayMap) continue;
92
-
93
- // Check if rawData contains a FK field matching the parent model name
94
- const fkValue = rawData[parentModelName];
95
- if (fkValue === undefined || fkValue === null) continue;
96
-
97
- const parentArray = childArrayMap.get(fkValue);
98
- if (parentArray && !parentArray.includes(record)) {
99
- parentArray.push(record);
100
- }
101
- }
102
- }
103
-
104
82
  // Fulfill pending belongsTo relationships
105
83
  const pendingBelongsToQueue = getPendingBelongsToRegistry();
106
84
  const pendingBelongsToRaw = pendingBelongsToQueue.get(modelName)?.get(record.id);
@@ -108,7 +86,7 @@ export function createRecord(modelName: string, rawData: { [key: string]: unknow
108
86
 
109
87
  if (pendingBelongsTo) {
110
88
  const belongsToReg = getBelongsToRegistry();
111
- const pendingHasManyReg = getHasManyRegistry();
89
+ const hasManyReg = getHasManyRegistry();
112
90
 
113
91
  for (const { sourceRecord, sourceModelName, relationshipKey, relationshipId } of pendingBelongsTo) {
114
92
  // Update the belongsTo relationship on the source record
@@ -125,7 +103,7 @@ export function createRecord(modelName: string, rawData: { [key: string]: unknow
125
103
  }
126
104
 
127
105
  // Wire inverse hasMany if it exists
128
- const inverseHasMany = pendingHasManyReg.get(modelName)?.get(sourceModelName)?.get(record.id);
106
+ const inverseHasMany = hasManyReg.get(modelName)?.get(sourceModelName)?.get(record.id);
129
107
 
130
108
  if (inverseHasMany && !inverseHasMany.includes(sourceRecord)) {
131
109
  inverseHasMany.push(sourceRecord);
@@ -140,14 +118,22 @@ export function createRecord(modelName: string, rawData: { [key: string]: unknow
140
118
  const shouldPersist = orm?.sqlDb && !options.isDbRecord && !userOptions._relationshipKey && !options._skipAutoPersist;
141
119
  if (shouldPersist) {
142
120
  const response = { data: { id: record.id } };
143
- orm!.sqlDb!.persist('create', modelName, { rawData }, response).catch((err: unknown) => {
144
- orm!.emitPersistError({
145
- operation: 'create',
146
- modelName,
147
- recordId: record.id,
148
- error: err instanceof Error ? err : new Error(String(err)),
121
+ orm!.sqlDb!.persist('create', modelName, { rawData }, response)
122
+ .catch((err: unknown) => {
123
+ orm!.emitPersistError({
124
+ operation: 'create',
125
+ modelName,
126
+ recordId: record.id,
127
+ error: err instanceof Error ? err : new Error(String(err)),
128
+ });
129
+ })
130
+ .finally(() => {
131
+ // Evict non-memory records after persist to prevent unbounded heap growth (stonyx#81)
132
+ if (store._memoryResolver && !store._memoryResolver(modelName)) {
133
+ const ms = store.get(modelName);
134
+ if (ms) ms.delete(record.id as number | string);
135
+ }
149
136
  });
150
- });
151
137
  }
152
138
 
153
139
  return record;
package/src/serializer.ts CHANGED
@@ -94,33 +94,8 @@ export default class Serializer {
94
94
  const handlerOptions = { ...options, _relationshipKey: key };
95
95
  const childRecord = handler(record, data, handlerOptions);
96
96
 
97
- // hasMany relationships use a getter so format()/toJSON() always read
98
- // the live registry array instead of a stale snapshot captured at
99
- // serialization time. This is critical when child records are created
100
- // in a later async frame — the belongsTo inverse wiring pushes into
101
- // the shared registry array, and the getter ensures the parent sees it.
102
- const isHasMany = (handler as { __relationshipType?: string }).__relationshipType === 'hasMany';
103
-
104
- if (isHasMany) {
105
- // `childRecord` IS the shared registry array — define a getter that
106
- // always dereferences through the same array reference.
107
- const registryArray = childRecord as unknown[];
108
- Object.defineProperty(rec, key, {
109
- enumerable: true,
110
- configurable: true,
111
- get: () => registryArray,
112
- set(v: unknown) { relatedRecords[key] = v; }
113
- });
114
- Object.defineProperty(relatedRecords, key, {
115
- enumerable: true,
116
- configurable: true,
117
- get: () => registryArray,
118
- set(v: unknown) { Object.defineProperty(relatedRecords, key, { value: v, writable: true, enumerable: true, configurable: true }); }
119
- });
120
- } else {
121
- rec[key] = childRecord;
122
- relatedRecords[key] = childRecord;
123
- }
97
+ rec[key] = childRecord;
98
+ relatedRecords[key] = childRecord;
124
99
 
125
100
  continue;
126
101
  }