@warp-drive/core 5.7.0-alpha.3 → 5.7.0-alpha.5

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.
Files changed (34) hide show
  1. package/declarations/reactive/-private/default-mode.d.ts +73 -0
  2. package/declarations/reactive/-private/fields/get-field-key.d.ts +8 -0
  3. package/declarations/reactive/-private/fields/managed-array.d.ts +3 -5
  4. package/declarations/reactive/-private/fields/managed-object.d.ts +5 -3
  5. package/declarations/reactive/-private/kind/alias-field.d.ts +4 -0
  6. package/declarations/reactive/-private/kind/array-field.d.ts +4 -0
  7. package/declarations/reactive/-private/kind/attribute-field.d.ts +4 -0
  8. package/declarations/reactive/-private/kind/belongs-to-field.d.ts +4 -0
  9. package/declarations/reactive/-private/kind/collection-field.d.ts +4 -0
  10. package/declarations/reactive/-private/kind/derived-field.d.ts +4 -0
  11. package/declarations/reactive/-private/kind/generic-field.d.ts +4 -0
  12. package/declarations/reactive/-private/kind/has-many-field.d.ts +4 -0
  13. package/declarations/reactive/-private/kind/hash-field.d.ts +4 -0
  14. package/declarations/reactive/-private/kind/identity-field.d.ts +4 -0
  15. package/declarations/reactive/-private/kind/local-field.d.ts +4 -0
  16. package/declarations/reactive/-private/kind/object-field.d.ts +4 -0
  17. package/declarations/reactive/-private/kind/resource-field.d.ts +4 -0
  18. package/declarations/reactive/-private/kind/schema-array-field.d.ts +4 -0
  19. package/declarations/reactive/-private/kind/schema-object-field.d.ts +4 -0
  20. package/declarations/reactive/-private/record.d.ts +2 -4
  21. package/declarations/reactive/-private/schema.d.ts +6 -2
  22. package/declarations/reactive.d.ts +1 -0
  23. package/declarations/store/-types/q/schema-service.d.ts +20 -32
  24. package/declarations/types/schema/fields.d.ts +348 -11
  25. package/dist/graph/-private.js +1 -1
  26. package/dist/{handler-D2jjnIA-.js → handler-SdXlte1w.js} +1 -1
  27. package/dist/index.js +2 -2
  28. package/dist/reactive.js +1156 -584
  29. package/dist/{request-state-CejVJgdj.js → request-state-CeN66aML.js} +12 -10
  30. package/dist/store/-private.js +2 -2
  31. package/dist/types/-private.js +1 -1
  32. package/dist/types/schema/fields.js +17 -2
  33. package/package.json +3 -3
  34. package/declarations/reactive/-private/fields/compute.d.ts +0 -43
package/dist/reactive.js CHANGED
@@ -1,14 +1,29 @@
1
1
  import { isResourceSchema } from './types/schema/fields.js';
2
- import { F as withSignalStore, T as isExtensionProp, U as performExtensionSet, H as consumeInternalSignal, V as performArrayExtensionGet, x as entangleSignal, W as performObjectExtensionGet, d as SOURCE$1, f as fastPush, y as defineSignal, l as RelatedCollection, J as getOrCreateInternalSignal, G as notifyInternalSignal, B as Signals, h as setRecordIdentifier, r as recordIdentifierFor } from "./request-state-CejVJgdj.js";
2
+ import { F as withSignalStore, T as isExtensionProp, U as performExtensionSet, H as consumeInternalSignal, V as performArrayExtensionGet, x as entangleSignal, E as peekInternalSignal, d as SOURCE$1, f as fastPush, l as RelatedCollection, J as getOrCreateInternalSignal, G as notifyInternalSignal, W as performObjectExtensionGet, y as defineSignal, B as Signals, h as setRecordIdentifier, r as recordIdentifierFor } from "./request-state-CeN66aML.js";
3
3
  import { EnableHydration, STRUCTURED } from './types/request.js';
4
4
  import { macroCondition, getGlobalConfig } from '@embroider/macros';
5
5
  import { warn, deprecate } from '@ember/debug';
6
6
  import './utils/string.js';
7
7
  import { A as ARRAY_SIGNAL, O as OBJECT_SIGNAL, c as createMemo } from "./configure-B48bFHOl.js";
8
8
  import { RecordStore, Type } from './types/symbols.js';
9
+ import { S as SOURCE, E as Editable, L as Legacy, D as Destroy, I as Identifier, P as Parent, a as EmbeddedPath, C as Checkout, b as EmbeddedField } from "./symbols-SIstXMLI.js";
9
10
  import { getOrSetGlobal } from './types/-private.js';
10
- import { S as SOURCE, E as Editable, L as Legacy, I as Identifier, P as Parent, a as EmbeddedPath, D as Destroy, C as Checkout, b as EmbeddedField } from "./symbols-SIstXMLI.js";
11
11
  import './index.js';
12
+ function getAliasField(context) {
13
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
14
+ {
15
+ throw new Error(`Alias field access is not implemented`);
16
+ }
17
+ })() : {};
18
+ }
19
+ function setAliasField(context) {
20
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
21
+ {
22
+ throw new Error(`Alias field setting is not implemented`);
23
+ }
24
+ })() : {};
25
+ return false;
26
+ }
12
27
  const ARRAY_GETTER_METHODS = new Set([Symbol.iterator, 'concat', 'entries', 'every', 'fill', 'filter', 'find', 'findIndex', 'flat', 'flatMap', 'forEach', 'includes', 'indexOf', 'join', 'keys', 'lastIndexOf', 'map', 'reduce', 'reduceRight', 'slice', 'some', 'values']);
13
28
  // const ARRAY_SETTER_METHODS = new Set<KeyType>(['push', 'pop', 'unshift', 'shift', 'splice', 'sort']);
14
29
  const SYNC_PROPS = new Set(['[]', 'length']);
@@ -55,21 +70,27 @@ function safeForEach(instance, arr, store, callback, target) {
55
70
  }
56
71
  // eslint-disable-next-line @typescript-eslint/no-extraneous-class
57
72
  class ManagedArray {
58
- constructor(store, schema, cache, field, data, identifier, path, owner, isSchemaArray, editable, legacy) {
73
+ constructor(context, owner, data) {
59
74
  // eslint-disable-next-line @typescript-eslint/no-this-alias
60
75
  const self = this;
61
76
  this[SOURCE] = data?.slice();
62
- const IS_EDITABLE = this[Editable] = editable ?? false;
63
- this[Legacy] = legacy;
77
+ const IS_EDITABLE = this[Editable] = context.editable ?? false;
78
+ this[Legacy] = context.legacy;
79
+ const schema = context.store.schema;
80
+ const cache = context.store.cache;
81
+ const {
82
+ field
83
+ } = context;
64
84
  const signals = withSignalStore(this);
65
85
  let _SIGNAL = null;
66
86
  const boundFns = new Map();
67
- this.identifier = identifier;
68
- this.path = path;
87
+ this.identifier = context.resourceKey;
88
+ this.path = context.path;
69
89
  this.owner = owner;
70
90
  let transaction = false;
71
- const mode = field.options?.key ?? '@identity';
72
- const RefStorage = mode === '@identity' ? WeakMap :
91
+ const KeyMode = field.options?.key ?? '@identity';
92
+ // listener.
93
+ const RefStorage = KeyMode === '@identity' ? WeakMap :
73
94
  // CAUTION CAUTION CAUTION
74
95
  // this is a pile of lies
75
96
  // the Map is Map<string, WeakRef<ReactiveResource>>
@@ -77,8 +98,8 @@ class ManagedArray {
77
98
  // internal to a method like ours without us duplicating the code
78
99
  // into two separate methods.
79
100
  Map;
80
- const ManagedRecordRefs = isSchemaArray ? new RefStorage() : null;
81
- const extensions = legacy ? schema.CAUTION_MEGA_DANGER_ZONE_arrayExtensions(field) : null;
101
+ const ManagedRecordRefs = field.kind === 'schema-array' ? new RefStorage() : null;
102
+ const extensions = context.legacy ? schema.CAUTION_MEGA_DANGER_ZONE_arrayExtensions(field) : null;
82
103
  const proxy = new Proxy(this[SOURCE], {
83
104
  get(target, prop, receiver) {
84
105
  if (prop === ARRAY_SIGNAL) {
@@ -93,7 +114,7 @@ class ManagedArray {
93
114
  const index = convertToInt(prop);
94
115
  if (_SIGNAL.isStale && (index !== null || SYNC_PROPS.has(prop) || isArrayGetter(prop))) {
95
116
  _SIGNAL.isStale = false;
96
- const newData = cache.getAttr(identifier, path);
117
+ const newData = cache.getAttr(context.resourceKey, context.path);
97
118
  if (newData && newData !== self[SOURCE]) {
98
119
  self[SOURCE].length = 0;
99
120
  self[SOURCE].push(...newData);
@@ -104,14 +125,72 @@ class ManagedArray {
104
125
  }
105
126
  if (prop === '[]') return consumeInternalSignal(_SIGNAL), receiver;
106
127
  if (index !== null) {
107
- let val;
108
- if (mode === '@hash') {
109
- val = target[index];
128
+ if (!transaction) {
129
+ consumeInternalSignal(_SIGNAL);
130
+ }
131
+ const rawValue = target[index];
132
+ if (field.kind === 'array') {
133
+ if (field.type) {
134
+ const transform = schema.transformation(field);
135
+ return transform.hydrate(rawValue, field.options ?? null, self.owner);
136
+ }
137
+ return rawValue;
138
+ }
139
+
140
+ /**
141
+ * When the array is polymorphic, we need to determine the real type
142
+ * in order to apply the correct identity as schema-object identity
143
+ * is only required to be unique by type
144
+ */
145
+ let objectType;
146
+ if (field.options?.polymorphic) {
147
+ const typePath = field.options.type ?? 'type';
148
+ // if we are polymorphic, then context.field.options.type will
149
+ // either specify a path on the rawValue to use as the type, defaulting to "type" or
150
+ // the special string "@hash" which tells us to treat field.type as a hashFn name with which
151
+ // to calc the type.
152
+ if (typePath === '@hash') {
153
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
154
+ if (!test) {
155
+ throw new Error(`Expected the field to define a hashFn as its type`);
156
+ }
157
+ })(field.type) : {};
158
+ const hashFn = schema.hashFn({
159
+ type: field.type
160
+ });
161
+ // TODO consider if there are better options and name args we could provide.
162
+ objectType = hashFn(rawValue, null, null);
163
+ } else {
164
+ objectType = rawValue[typePath];
165
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
166
+ if (!test) {
167
+ throw new Error(`Expected the type path for the field to be a value on the raw object`);
168
+ }
169
+ })(typePath && objectType && typeof objectType === 'string') : {};
170
+ }
171
+ } else {
172
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
173
+ if (!test) {
174
+ throw new Error(`A non-polymorphic SchemaArrayField must provide a SchemaObject type in its definition`);
175
+ }
176
+ })(field.type) : {};
177
+ objectType = field.type;
178
+ }
179
+
180
+ /**
181
+ * When KeyMode=@hash the ReactiveResource is keyed into
182
+ * ManagedRecordRefs by the return value of @hash on the rawValue.
183
+ *
184
+ * This means that we could find a way to only recompute the identity
185
+ * when ARRAY_SIGNAL is dirty if hash performance becomes a bottleneck.
186
+ */
187
+ let schemaObjectKeyValue;
188
+ if (KeyMode === '@hash') {
110
189
  const hashField = schema.resource({
111
- type: field.type
190
+ type: objectType
112
191
  }).identity;
113
192
  const hashFn = schema.hashFn(hashField);
114
- val = hashFn(val, null, null);
193
+ schemaObjectKeyValue = hashFn(rawValue, hashField.options ?? null, hashField.name);
115
194
  } else {
116
195
  // if mode is not @identity or @index, then access the key path.
117
196
  // we should assert that `mode` is a string
@@ -119,69 +198,78 @@ class ManagedArray {
119
198
  // and, we likely should lookup the associated field and throw an error IF
120
199
  // the given field does not exist OR
121
200
  // the field is anything other than a GenericField or LegacyAttributeField.
122
- if (mode !== '@identity' && mode !== '@index') {
123
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
124
- if (!test) {
125
- throw new Error('mode must be a string');
126
- }
127
- })(typeof mode === 'string') : {};
128
- const modeField = schema.resource({
129
- type: field.type
130
- }).fields.find(f => f.name === mode);
131
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
132
- if (!test) {
133
- throw new Error('field must exist in schema');
134
- }
135
- })(modeField) : {};
136
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
137
- if (!test) {
138
- throw new Error('field must be a GenericField or LegacyAttributeField');
139
- }
140
- })(modeField.kind === 'field' || modeField.kind === 'attribute') : {};
141
- }
142
- val = mode === '@identity' ? target[index] : mode === '@index' ? '@index' : target[index][mode];
143
- }
144
- if (isSchemaArray) {
145
- if (!transaction) {
146
- consumeInternalSignal(_SIGNAL);
147
- }
148
- if (val) {
149
- const recordRef = ManagedRecordRefs.get(val);
150
- let record = recordRef?.deref();
151
- if (!record) {
152
- const recordPath = path.slice();
153
- // this is a dirty lie since path is string[] but really we
154
- // should change the types for paths to `Array<string | number>`
155
- // TODO we should allow the schema for the field to define a "key"
156
- // for stability. Default should be `@identity` which means that
157
- // same object reference from cache should result in same ReactiveResource
158
- // embedded object.
159
- recordPath.push(index);
160
- const recordIdentifier = self.owner[Identifier] || self.owner[Parent];
161
- record = new ReactiveResource(store, recordIdentifier, {
162
- [Editable]: self.owner[Editable],
163
- [Legacy]: self.owner[Legacy]
164
- }, true, field, recordPath);
165
- // if mode is not @identity or @index, then access the key path now
166
- // to determine the key value.
167
- // chris says we can implement this as a special kind `@hash` which
168
- // would be a function that only has access to the cache value and not
169
- // the record itself, so derivation is possible but intentionally limited
170
- // and non-reactive?
171
- ManagedRecordRefs.set(val, new WeakRef(record));
201
+ if (macroCondition(getGlobalConfig().WarpDrive.env.DEBUG)) {
202
+ const isPathKeyMode = KeyMode !== '@identity' && KeyMode !== '@index';
203
+ if (isPathKeyMode) {
204
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
205
+ if (!test) {
206
+ throw new Error('mode must be a string');
207
+ }
208
+ })(typeof KeyMode === 'string' && KeyMode !== '') : {};
209
+ const modeField = schema.fields({
210
+ type: objectType
211
+ }).get(KeyMode);
212
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
213
+ if (!test) {
214
+ throw new Error('field must exist in schema');
215
+ }
216
+ })(modeField) : {};
217
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
218
+ if (!test) {
219
+ throw new Error('field must be a GenericField or LegacyAttributeField');
220
+ }
221
+ })(modeField.kind === 'field' || modeField.kind === 'attribute') : {};
172
222
  }
173
- return record;
174
223
  }
175
- return val;
224
+ schemaObjectKeyValue = KeyMode === '@identity' ? rawValue : KeyMode === '@index' ? index : rawValue[KeyMode];
176
225
  }
177
- if (!transaction) {
178
- consumeInternalSignal(_SIGNAL);
226
+ if (!schemaObjectKeyValue) {
227
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
228
+ {
229
+ throw new Error(`Unexpected out of bounds access on SchemaArray`);
230
+ }
231
+ })() : {};
232
+ return undefined;
179
233
  }
180
- if (field.type) {
181
- const transform = schema.transformation(field);
182
- return transform.hydrate(val, field.options ?? null, self.owner);
234
+ const recordRef = ManagedRecordRefs.get(schemaObjectKeyValue);
235
+ const record = recordRef?.value.deref();
236
+
237
+ // confirm the type and key still match
238
+ if (record && recordRef.type === objectType && recordRef.identity === schemaObjectKeyValue) {
239
+ if (recordRef.index !== index) {
240
+ recordRef.index = index;
241
+ recordRef.context.path[recordRef.context.path.length - 1] = index;
242
+ }
243
+ return record;
244
+ } else if (record) {
245
+ // TODO schedule idle once we can
246
+ void Promise.resolve().then(() => {
247
+ record[Destroy]();
248
+ });
183
249
  }
184
- return val;
250
+ const recordPath = context.path.slice();
251
+ // this is a dirty lie since path is string[] but really we
252
+ // should change the types for paths to `Array<string | number>`
253
+ recordPath.push(index);
254
+ const objectContext = {
255
+ store: context.store,
256
+ resourceKey: context.resourceKey,
257
+ modeName: context.modeName,
258
+ legacy: context.legacy,
259
+ editable: context.editable,
260
+ path: recordPath,
261
+ field: field,
262
+ value: objectType
263
+ };
264
+ const schemaObject = new ReactiveResource(objectContext);
265
+ ManagedRecordRefs.set(schemaObjectKeyValue, {
266
+ type: objectType,
267
+ identity: schemaObjectKeyValue,
268
+ index,
269
+ context: objectContext,
270
+ value: new WeakRef(schemaObject)
271
+ });
272
+ return schemaObject;
185
273
  }
186
274
  if (isArrayGetter(prop)) {
187
275
  let fn = boundFns.get(prop);
@@ -190,7 +278,7 @@ class ManagedArray {
190
278
  fn = function () {
191
279
  consumeInternalSignal(_SIGNAL);
192
280
  transaction = true;
193
- const result = safeForEach(receiver, target, store, arguments[0], arguments[1]);
281
+ const result = safeForEach(receiver, target, context.store, arguments[0], arguments[1]);
194
282
  transaction = false;
195
283
  return result;
196
284
  };
@@ -233,9 +321,9 @@ class ManagedArray {
233
321
  },
234
322
  set(target, prop, value, receiver) {
235
323
  if (!IS_EDITABLE) {
236
- let errorPath = identifier.type;
237
- if (path) {
238
- errorPath = path[path.length - 1];
324
+ let errorPath = context.resourceKey.type;
325
+ if (context.path) {
326
+ errorPath = context.path[context.path.length - 1];
239
327
  }
240
328
  throw new Error(`Cannot set ${String(prop)} on ${errorPath} because the record is not editable`);
241
329
  }
@@ -253,19 +341,19 @@ class ManagedArray {
253
341
  const reflect = Reflect.set(target, prop, value, receiver);
254
342
  if (reflect) {
255
343
  if (!field.type) {
256
- cache.setAttr(identifier, path, self[SOURCE]);
344
+ cache.setAttr(context.resourceKey, context.path, self[SOURCE]);
257
345
  _SIGNAL.isStale = true;
258
346
  return true;
259
347
  }
260
348
  let rawValue = self[SOURCE];
261
- if (!isSchemaArray) {
349
+ if (field.kind !== 'schema-array') {
262
350
  const transform = schema.transformation(field);
263
351
  if (!transform) {
264
- throw new Error(`No '${field.type}' transform defined for use by ${identifier.type}.${String(prop)}`);
352
+ throw new Error(`No '${field.type}' transform defined for use by ${context.resourceKey.type}.${String(prop)}`);
265
353
  }
266
354
  rawValue = self[SOURCE].map(item => transform.serialize(item, field.options ?? null, self.owner));
267
355
  }
268
- cache.setAttr(identifier, path, rawValue);
356
+ cache.setAttr(context.resourceKey, context.path, rawValue);
269
357
  _SIGNAL.isStale = true;
270
358
  }
271
359
  return reflect;
@@ -302,27 +390,453 @@ const desc = {
302
390
  };
303
391
  // compat(desc);
304
392
  Object.defineProperty(ManagedArray.prototype, '[]', desc);
393
+ function getArrayField(context) {
394
+ const signal = entangleSignal(context.signals, context.record, context.path.at(-1), null);
395
+ // the thing we hand out needs to know its owner and path in a private manner
396
+ // its "address" is the parent identifier (identifier) + field name (field.name)
397
+ // in the nested object case field name here is the full dot path from root resource to this value
398
+ // its "key" is the field on the parent record
399
+ // its "owner" is the parent record
400
+ const {
401
+ record
402
+ } = context;
403
+ let managedArray = signal.value;
404
+ if (managedArray) {
405
+ return managedArray;
406
+ } else {
407
+ const {
408
+ store,
409
+ resourceKey,
410
+ path
411
+ } = context;
412
+ const {
413
+ cache
414
+ } = store;
415
+ const rawValue = context.editable ? cache.getAttr(resourceKey, path) : cache.getRemoteAttr(resourceKey, path);
416
+ if (!rawValue) {
417
+ return null;
418
+ }
419
+ managedArray = new ManagedArray(context, record, rawValue);
420
+ signal.value = managedArray;
421
+ }
422
+ return managedArray;
423
+ }
424
+ function setArrayField(context) {
425
+ const {
426
+ field,
427
+ record,
428
+ value
429
+ } = context;
430
+ const {
431
+ cache,
432
+ schema
433
+ } = context.store;
434
+ const fieldSignal = peekInternalSignal(context.signals, context.path.at(-1));
435
+ const peeked = fieldSignal?.value;
436
+ const transform = field.type ? schema.transformation(field) : null;
437
+ const rawValue = field.type ? value.map(item => transform.serialize(item, field.options ?? null, record)) : value?.slice();
438
+ cache.setAttr(context.resourceKey, context.path, rawValue);
439
+ if (peeked) {
440
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
441
+ if (!test) {
442
+ throw new Error(`Expected the peekManagedArray for ${field.kind} to return a ManagedArray`);
443
+ }
444
+ })(ARRAY_SIGNAL in peeked) : {};
445
+ const arrSignal = peeked[ARRAY_SIGNAL];
446
+ arrSignal.isStale = true;
447
+ // TODO run array destroy?
448
+ }
449
+ if (!Array.isArray(rawValue) && fieldSignal) {
450
+ fieldSignal.value = null;
451
+ }
452
+ return true;
453
+ }
454
+ function getAttributeField(context) {
455
+ entangleSignal(context.signals, context.record, context.path.at(-1), null);
456
+ const {
457
+ cache
458
+ } = context.store;
459
+ return context.editable ? cache.getAttr(context.resourceKey, context.path) : cache.getRemoteAttr(context.resourceKey, context.path);
460
+ }
461
+ function setAttributeField(context) {
462
+ context.store.cache.setAttr(context.resourceKey, context.path, context.value);
463
+ return true;
464
+ }
465
+ const InvalidKinds = ['alias', 'derived', '@local'];
466
+ function isInvalidKind(kind) {
467
+ return InvalidKinds.includes(kind);
468
+ }
469
+ function isNonIdentityCacheableField(field) {
470
+ return !isInvalidKind(field.kind) && field.kind !== '@id' && field.kind !== '@hash';
471
+ }
472
+ function getFieldCacheKeyStrict(field) {
473
+ return field.sourceKey || field.name;
474
+ }
475
+ function getFieldCacheKey(field) {
476
+ return 'sourceKey' in field && field.sourceKey ? field.sourceKey : field.name;
477
+ }
478
+ function getBelongsToField(context) {
479
+ entangleSignal(context.signals, context.record, context.path.at(-1), null);
480
+ const {
481
+ field,
482
+ resourceKey,
483
+ store
484
+ } = context;
485
+ const {
486
+ schema,
487
+ cache
488
+ } = store;
489
+ if (field.options.linksMode) {
490
+ const rawValue = context.editable ? cache.getRelationship(resourceKey, getFieldCacheKeyStrict(field)) : cache.getRemoteRelationship(resourceKey, getFieldCacheKeyStrict(field));
491
+ return rawValue.data ? store.peekRecord(rawValue.data) : null;
492
+ }
493
+
494
+ // FIXME move this to a "LegacyMode" make this part of "PolarisMode"
495
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
496
+ if (!test) {
497
+ throw new Error(`Can only use belongsTo fields when the resource is in legacy mode`);
498
+ }
499
+ })(context.legacy) : {};
500
+ return schema._kind('@legacy', 'belongsTo').get(store, context.record, resourceKey, field);
501
+ }
502
+ function setBelongsToField(context) {
503
+ const {
504
+ store
505
+ } = context;
506
+ const {
507
+ schema
508
+ } = store;
509
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
510
+ if (!test) {
511
+ throw new Error(`Can only mutate belongsTo fields when the resource is in legacy mode`);
512
+ }
513
+ })(context.legacy) : {};
514
+ schema._kind('@legacy', 'belongsTo').set(store, context.record, context.resourceKey, context.field, context.value);
515
+ return true;
516
+ }
517
+ function getCollectionField(context) {
518
+ entangleSignal(context.signals, context.record, context.path.at(-1), null);
519
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
520
+ {
521
+ throw new Error(`Accessing collection fields is not yet implemented`);
522
+ }
523
+ })() : {};
524
+ }
525
+ function setCollectionField(context) {
526
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
527
+ {
528
+ throw new Error(`Setting collection fields is not yet implemented`);
529
+ }
530
+ })() : {};
531
+ return false;
532
+ }
533
+ function getDerivedField(context) {
534
+ const {
535
+ schema
536
+ } = context.store;
537
+ return schema.derivation(context.field)(context.record, context.field.options ?? null, context.field.name);
538
+ }
539
+ function setDerivedField(context) {
540
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
541
+ {
542
+ throw new Error(`ILLEGAL SET: Cannot set '${context.path.join('.')}' on '${context.resourceKey.type}' as ${context.field.kind} fields are not mutable`);
543
+ }
544
+ })() : {};
545
+ return false;
546
+ }
547
+ function getGenericField(context) {
548
+ entangleSignal(context.signals, context.record, context.path.at(-1), null);
549
+ const {
550
+ cache,
551
+ schema
552
+ } = context.store;
553
+ const rawValue = context.editable ? cache.getAttr(context.resourceKey, context.path) : cache.getRemoteAttr(context.resourceKey, context.path);
554
+ const {
555
+ field
556
+ } = context;
557
+ if (!field.type) {
558
+ return rawValue;
559
+ }
560
+ const transform = schema.transformation(field);
561
+ return transform.hydrate(rawValue, field.options ?? null, context.record);
562
+ }
563
+ function setGenericField(context) {
564
+ const {
565
+ cache,
566
+ schema
567
+ } = context.store;
568
+ const {
569
+ field
570
+ } = context;
571
+ if (!field.type) {
572
+ cache.setAttr(context.resourceKey, context.path, context.value);
573
+ return true;
574
+ }
575
+ const transform = schema.transformation(field);
576
+ const rawValue = transform.serialize(context.value, field.options ?? null, context.record);
577
+ cache.setAttr(context.resourceKey, context.path, rawValue);
578
+ return true;
579
+ }
580
+ class ManyArrayManager {
581
+ constructor(record, editable) {
582
+ this.record = record;
583
+ this.store = record[RecordStore];
584
+ this.identifier = record[Identifier];
585
+ this.editable = editable;
586
+ }
587
+ _syncArray(array) {
588
+ const method = this.editable ? 'getRelationship' : 'getRemoteRelationship';
589
+ // FIXME field needs to use sourceKey
590
+ const rawValue = this.store.cache[method](this.identifier, array.key);
591
+ if (rawValue.meta) {
592
+ array.meta = rawValue.meta;
593
+ }
594
+ if (rawValue.links) {
595
+ array.links = rawValue.links;
596
+ }
597
+ const currentState = array[SOURCE$1];
598
+
599
+ // unlike in the normal RecordArray case, we don't need to divorce the reference
600
+ // because we don't need to worry about associate/disassociate since the graph
601
+ // takes care of that for us
602
+ if (currentState !== rawValue.data) {
603
+ currentState.length = 0;
604
+ fastPush(currentState, rawValue.data);
605
+ }
606
+ }
607
+ reloadHasMany(key, options) {
608
+ // FIXME field needs to use sourceKey
609
+ const field = this.store.schema.fields(this.identifier).get(key);
610
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
611
+ if (!test) {
612
+ throw new Error(`Expected a hasMany field for ${key}`);
613
+ }
614
+ })(field?.kind === 'hasMany') : {};
615
+ const cacheOptions = options ? extractCacheOptions(options) : {
616
+ reload: true
617
+ };
618
+ cacheOptions.types = [field.type];
619
+ const rawValue = this.store.cache.getRelationship(this.identifier, key);
620
+ const req = {
621
+ url: getRelatedLink(rawValue),
622
+ op: 'findHasMany',
623
+ method: 'GET',
624
+ records: rawValue.data,
625
+ cacheOptions,
626
+ options: {
627
+ field,
628
+ identifier: this.identifier,
629
+ links: rawValue.links,
630
+ meta: rawValue.meta
631
+ },
632
+ [EnableHydration]: false
633
+ };
634
+ return this.store.request(req);
635
+ }
636
+ mutate(mutation) {
637
+ this.store.cache.mutate(mutation);
638
+ }
639
+ }
640
+ function getRelatedLink(resource) {
641
+ const related = resource.links?.related;
642
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
643
+ if (!test) {
644
+ throw new Error(`Expected a related link`);
645
+ }
646
+ })(related) : {};
647
+ return typeof related === 'object' ? related.href : related;
648
+ }
649
+ function extractCacheOptions(options) {
650
+ const cacheOptions = {};
651
+ if ('reload' in options) {
652
+ cacheOptions.reload = options.reload;
653
+ }
654
+ if ('backgroundReload' in options) {
655
+ cacheOptions.backgroundReload = options.backgroundReload;
656
+ }
657
+ return cacheOptions;
658
+ }
659
+ function getHasManyField(context) {
660
+ const signal = entangleSignal(context.signals, context.record, context.path.at(-1), null);
661
+ const {
662
+ store,
663
+ field
664
+ } = context;
665
+ if (field.options.linksMode) {
666
+ const {
667
+ record
668
+ } = context;
669
+ // the thing we hand out needs to know its owner and path in a private manner
670
+ // its "address" is the parent identifier (identifier) + field name (field.name)
671
+ // in the nested object case field name here is the full dot path from root resource to this value
672
+ // its "key" is the field on the parent record
673
+ // its "owner" is the parent record
674
+
675
+ const cached = signal.value;
676
+ if (cached) {
677
+ return cached;
678
+ }
679
+ const {
680
+ editable,
681
+ resourceKey
682
+ } = context;
683
+ const {
684
+ cache
685
+ } = store;
686
+ const rawValue = cache.getRelationship(resourceKey, getFieldCacheKeyStrict(field));
687
+ if (!rawValue) {
688
+ return null;
689
+ }
690
+ const managedArray = new RelatedCollection({
691
+ store,
692
+ type: field.type,
693
+ identifier: resourceKey,
694
+ cache,
695
+ field: context.legacy ? field : undefined,
696
+ // we divorce the reference here because ManyArray mutates the target directly
697
+ // before sending the mutation op to the cache. We may be able to avoid this in the future
698
+ identifiers: rawValue.data?.slice(),
699
+ key: field.name,
700
+ meta: rawValue.meta || null,
701
+ links: rawValue.links || null,
702
+ isPolymorphic: field.options.polymorphic ?? false,
703
+ isAsync: field.options.async ?? false,
704
+ // TODO: Grab the proper value
705
+ _inverseIsAsync: false,
706
+ // @ts-expect-error Typescript doesn't have a way for us to thread the generic backwards so it infers unknown instead of T
707
+ manager: new ManyArrayManager(record, editable),
708
+ isLoaded: true,
709
+ allowMutation: editable
710
+ });
711
+ signal.value = managedArray;
712
+ return managedArray;
713
+ }
714
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
715
+ if (!test) {
716
+ throw new Error(`Can only use hasMany fields when the resource is in legacy mode`);
717
+ }
718
+ })(context.legacy) : {};
719
+ return store.schema._kind('@legacy', 'hasMany').get(store, context.record, context.resourceKey, field);
720
+ }
721
+ function setHasManyField(context) {
722
+ const {
723
+ store
724
+ } = context;
725
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
726
+ if (!test) {
727
+ throw new Error(`Can only use hasMany fields when the resource is in legacy mode`);
728
+ }
729
+ })(context.legacy) : {};
730
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
731
+ if (!test) {
732
+ throw new Error(`You must pass an array of records to set a hasMany relationship`);
733
+ }
734
+ })(Array.isArray(context.value)) : {};
735
+ store.schema._kind('@legacy', 'hasMany').set(store, context.record, context.resourceKey, context.field, context.value);
736
+ return true;
737
+ }
738
+ function getHashField(context) {
739
+ const {
740
+ field,
741
+ path,
742
+ resourceKey
743
+ } = context;
744
+ const {
745
+ schema,
746
+ cache
747
+ } = context.store;
748
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
749
+ if (!test) {
750
+ throw new Error(`Cannot use a ${field.kind} directly on a resource.`);
751
+ }
752
+ })(Array.isArray(path) && path.length > 1) : {};
753
+ const realPath = path.slice(0, -1);
754
+ const rawData = context.editable ? cache.getAttr(resourceKey, realPath) : cache.getRemoteAttr(resourceKey, realPath);
755
+ return schema.hashFn(field)(rawData, field.options ?? null, field.name ?? null);
756
+ }
757
+ function setHashField(context) {
758
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
759
+ {
760
+ throw new Error(`ILLEGAL SET: Cannot set '${Array.isArray(context.path) ? context.path.join('.') : context.path}' on '${context.resourceKey.type}' as ${context.field.kind} fields are not mutable`);
761
+ }
762
+ })() : {};
763
+ return false;
764
+ }
765
+ function getIdentityField(context) {
766
+ entangleSignal(context.signals, context.record, '@identity', null);
767
+ return context.resourceKey.id;
768
+ }
769
+ function setIdentityField(context) {
770
+ const {
771
+ value,
772
+ resourceKey,
773
+ store
774
+ } = context;
775
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
776
+ if (!test) {
777
+ throw new Error(`Expected to receive a string id`);
778
+ }
779
+ })(typeof value === 'string' && value.length) : {};
780
+ const normalizedId = String(value);
781
+ const didChange = normalizedId !== resourceKey.id;
782
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
783
+ if (!test) {
784
+ throw new Error(`Cannot set ${resourceKey.type} record's id to ${normalizedId}, because id is already ${resourceKey.id}`);
785
+ }
786
+ })(!didChange || resourceKey.id === null) : {};
787
+ if (normalizedId !== null && didChange) {
788
+ store._instanceCache.setRecordId(resourceKey, normalizedId);
789
+ store.notifications.notify(resourceKey, 'identity');
790
+ }
791
+ return true;
792
+ }
793
+ function getLocalField(context) {
794
+ const {
795
+ field
796
+ } = context;
797
+ const signal = getOrCreateInternalSignal(context.signals, context.record, field.name, field.options?.defaultValue ?? null);
798
+ consumeInternalSignal(signal);
799
+ return signal.value;
800
+ }
801
+ function setLocalField(context) {
802
+ const {
803
+ value
804
+ } = context;
805
+ const signal = getOrCreateInternalSignal(context.signals, context.record, context.field.name, value);
806
+ if (signal.value !== value) {
807
+ signal.value = value;
808
+ notifyInternalSignal(signal);
809
+ }
810
+ return true;
811
+ }
305
812
  const ObjectSymbols = new Set([OBJECT_SIGNAL, Parent, SOURCE, Editable, EmbeddedPath]);
306
813
 
307
814
  // const ignoredGlobalFields = new Set<string>(['setInterval', 'nodeType', 'nodeName', 'length', 'document', STRUCTURED]);
308
815
 
309
816
  // eslint-disable-next-line @typescript-eslint/no-extraneous-class
310
817
  class ManagedObject {
311
- constructor(schema, cache, field, data, identifier, path, owner, editable, legacy) {
818
+ constructor(context) {
819
+ const {
820
+ field,
821
+ path
822
+ } = context;
312
823
  // eslint-disable-next-line @typescript-eslint/no-this-alias
313
824
  const self = this;
314
- this[SOURCE] = {
315
- ...data
316
- };
825
+ this[SOURCE] = Object.assign({}, context.value);
317
826
  const signals = withSignalStore(this);
318
827
  const _SIGNAL = this[OBJECT_SIGNAL] = entangleSignal(signals, this, OBJECT_SIGNAL, undefined);
319
- this[Editable] = editable;
320
- this[Legacy] = legacy;
321
- this[Parent] = identifier;
828
+ this[Editable] = context.editable;
829
+ this[Legacy] = context.legacy;
830
+ this[Parent] = context.resourceKey;
322
831
  this[EmbeddedPath] = path;
832
+ const identifier = context.resourceKey;
833
+ const {
834
+ cache,
835
+ schema
836
+ } = context.store;
323
837
 
324
838
  // prettier-ignore
325
- const extensions = !legacy ? null : schema.CAUTION_MEGA_DANGER_ZONE_objectExtensions(field);
839
+ const extensions = !context.legacy ? null : schema.CAUTION_MEGA_DANGER_ZONE_objectExtensions(field, null);
326
840
  const proxy = new Proxy(this[SOURCE], {
327
841
  ownKeys() {
328
842
  return Object.keys(self[SOURCE]);
@@ -332,7 +846,7 @@ class ManagedObject {
332
846
  },
333
847
  getOwnPropertyDescriptor(target, prop) {
334
848
  return {
335
- writable: editable,
849
+ writable: context.editable,
336
850
  enumerable: true,
337
851
  configurable: true
338
852
  };
@@ -366,11 +880,9 @@ class ManagedObject {
366
880
  if (newData && newData !== self[SOURCE]) {
367
881
  if (field.type) {
368
882
  const transform = schema.transformation(field);
369
- newData = transform.hydrate(newData, field.options ?? null, owner);
883
+ newData = transform.hydrate(newData, field.options ?? null, context.record);
370
884
  }
371
- self[SOURCE] = {
372
- ...newData
373
- }; // Add type assertion for newData
885
+ self[SOURCE] = Object.assign({}, newData); // Add type assertion for newData
374
886
  }
375
887
  }
376
888
 
@@ -396,7 +908,7 @@ class ManagedObject {
396
908
  if (!test) {
397
909
  throw new Error(`Cannot set read-only property '${String(prop)}' on ManagedObject`);
398
910
  }
399
- })(editable) : {};
911
+ })(context.editable) : {};
400
912
 
401
913
  // since objects function as dictionaries, we can't defer to schema/data before extensions
402
914
  // unless the prop is in the existing data.
@@ -411,151 +923,29 @@ class ManagedObject {
411
923
  cache.setAttr(identifier, path, self[SOURCE]);
412
924
  } else {
413
925
  const transform = schema.transformation(field);
414
- const val = transform.serialize(self[SOURCE], field.options ?? null, owner);
926
+ const val = transform.serialize(self[SOURCE], field.options ?? null, context.record);
415
927
  cache.setAttr(identifier, path, val);
416
928
  }
417
929
  _SIGNAL.isStale = true;
418
930
  return true;
419
- }
420
- });
421
- return proxy;
422
- }
423
- }
424
- class ManyArrayManager {
425
- constructor(record, editable) {
426
- this.record = record;
427
- this.store = record[RecordStore];
428
- this.identifier = record[Identifier];
429
- this.editable = editable;
430
- }
431
- _syncArray(array) {
432
- const method = this.editable ? 'getRelationship' : 'getRemoteRelationship';
433
- const rawValue = this.store.cache[method](this.identifier, array.key);
434
- if (rawValue.meta) {
435
- array.meta = rawValue.meta;
436
- }
437
- if (rawValue.links) {
438
- array.links = rawValue.links;
439
- }
440
- const currentState = array[SOURCE$1];
441
-
442
- // unlike in the normal RecordArray case, we don't need to divorce the reference
443
- // because we don't need to worry about associate/disassociate since the graph
444
- // takes care of that for us
445
- if (currentState !== rawValue.data) {
446
- currentState.length = 0;
447
- fastPush(currentState, rawValue.data);
448
- }
449
- }
450
- reloadHasMany(key, options) {
451
- const field = this.store.schema.fields(this.identifier).get(key);
452
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
453
- if (!test) {
454
- throw new Error(`Expected a hasMany field for ${key}`);
455
- }
456
- })(field?.kind === 'hasMany') : {};
457
- const cacheOptions = options ? extractCacheOptions(options) : {
458
- reload: true
459
- };
460
- cacheOptions.types = [field.type];
461
- const rawValue = this.store.cache.getRelationship(this.identifier, key);
462
- const req = {
463
- url: getRelatedLink(rawValue),
464
- op: 'findHasMany',
465
- method: 'GET',
466
- records: rawValue.data,
467
- cacheOptions,
468
- options: {
469
- field,
470
- identifier: this.identifier,
471
- links: rawValue.links,
472
- meta: rawValue.meta
473
- },
474
- [EnableHydration]: false
475
- };
476
- return this.store.request(req);
477
- }
478
- mutate(mutation) {
479
- this.store.cache.mutate(mutation);
480
- }
481
- }
482
- function getRelatedLink(resource) {
483
- const related = resource.links?.related;
484
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
485
- if (!test) {
486
- throw new Error(`Expected a related link`);
487
- }
488
- })(related) : {};
489
- return typeof related === 'object' ? related.href : related;
490
- }
491
- function extractCacheOptions(options) {
492
- const cacheOptions = {};
493
- if ('reload' in options) {
494
- cacheOptions.reload = options.reload;
495
- }
496
- if ('backgroundReload' in options) {
497
- cacheOptions.backgroundReload = options.backgroundReload;
931
+ }
932
+ });
933
+ return proxy;
498
934
  }
499
- return cacheOptions;
500
935
  }
501
- const ManagedArrayMap = getOrSetGlobal('ManagedArrayMap', new Map());
502
936
  const ManagedObjectMap = getOrSetGlobal('ManagedObjectMap', new Map());
503
- function computeLocal(record, field, prop) {
504
- const signals = withSignalStore(record);
505
- const signal = getOrCreateInternalSignal(signals, record, prop, field.options?.defaultValue ?? null);
506
- consumeInternalSignal(signal);
507
- return signal.value;
508
- }
509
- function peekManagedArray(record, field) {
510
- const managedArrayMapForRecord = ManagedArrayMap.get(record);
511
- if (managedArrayMapForRecord) {
512
- return managedArrayMapForRecord.get(field.name);
513
- }
514
- }
515
937
  function peekManagedObject(record, field) {
516
938
  const managedObjectMapForRecord = ManagedObjectMap.get(record);
517
939
  if (managedObjectMapForRecord) {
518
940
  return managedObjectMapForRecord.get(field.name);
519
941
  }
520
942
  }
521
- function computeField(schema, cache, record, identifier, field, prop, editable) {
522
- const rawValue = editable ? cache.getAttr(identifier, prop) : cache.getRemoteAttr(identifier, prop);
523
- if (!field.type) {
524
- return rawValue;
525
- }
526
- const transform = schema.transformation(field);
527
- return transform.hydrate(rawValue, field.options ?? null, record);
528
- }
529
- function computeArray(store, schema, cache, record, identifier, field, path, editable, legacy) {
530
- const isSchemaArray = field.kind === 'schema-array';
531
- // the thing we hand out needs to know its owner and path in a private manner
532
- // its "address" is the parent identifier (identifier) + field name (field.name)
533
- // in the nested object case field name here is the full dot path from root resource to this value
534
- // its "key" is the field on the parent record
535
- // its "owner" is the parent record
536
-
537
- const managedArrayMapForRecord = ManagedArrayMap.get(record);
538
- let managedArray;
539
- if (managedArrayMapForRecord) {
540
- managedArray = managedArrayMapForRecord.get(field.name);
541
- }
542
- if (managedArray) {
543
- return managedArray;
544
- } else {
545
- const rawValue = editable ? cache.getAttr(identifier, path) : cache.getRemoteAttr(identifier, path);
546
- if (!rawValue) {
547
- return null;
548
- }
549
- managedArray = new ManagedArray(store, schema, cache, field, rawValue, identifier, path, record, isSchemaArray, editable, legacy);
550
- if (!managedArrayMapForRecord) {
551
- ManagedArrayMap.set(record, new Map([[field.name, managedArray]]));
552
- } else {
553
- managedArrayMapForRecord.set(field.name, managedArray);
554
- }
555
- }
556
- return managedArray;
557
- }
558
- function computeObject(schema, cache, record, identifier, field, path, editable, legacy) {
943
+ function getObjectField(context) {
944
+ entangleSignal(context.signals, context.record, context.path.at(-1), null);
945
+ const {
946
+ record,
947
+ field
948
+ } = context;
559
949
  const managedObjectMapForRecord = ManagedObjectMap.get(record);
560
950
  let managedObject;
561
951
  if (managedObjectMapForRecord) {
@@ -564,7 +954,16 @@ function computeObject(schema, cache, record, identifier, field, path, editable,
564
954
  if (managedObject) {
565
955
  return managedObject;
566
956
  } else {
567
- let rawValue = editable ? cache.getAttr(identifier, path) : cache.getRemoteAttr(identifier, path);
957
+ const {
958
+ store,
959
+ resourceKey,
960
+ path
961
+ } = context;
962
+ const {
963
+ cache,
964
+ schema
965
+ } = store;
966
+ let rawValue = context.editable ? cache.getAttr(resourceKey, path) : cache.getRemoteAttr(resourceKey, path);
568
967
  if (!rawValue) {
569
968
  return null;
570
969
  }
@@ -572,7 +971,18 @@ function computeObject(schema, cache, record, identifier, field, path, editable,
572
971
  const transform = schema.transformation(field);
573
972
  rawValue = transform.hydrate(rawValue, field.options ?? null, record);
574
973
  }
575
- managedObject = new ManagedObject(schema, cache, field, rawValue, identifier, path, record, editable, legacy);
974
+ managedObject = new ManagedObject({
975
+ store,
976
+ resourceKey,
977
+ modeName: context.modeName,
978
+ legacy: context.legacy,
979
+ editable: context.editable,
980
+ path,
981
+ field,
982
+ record,
983
+ signals: context.signals,
984
+ value: rawValue
985
+ });
576
986
  if (!managedObjectMapForRecord) {
577
987
  ManagedObjectMap.set(record, new Map([[field.name, managedObject]]));
578
988
  } else {
@@ -581,48 +991,64 @@ function computeObject(schema, cache, record, identifier, field, path, editable,
581
991
  }
582
992
  return managedObject;
583
993
  }
584
- function computeSchemaObject(store, cache, record, identifier, field, path, legacy, editable) {
585
- const schemaObjectMapForRecord = ManagedObjectMap.get(record);
586
- let schemaObject;
587
- if (schemaObjectMapForRecord) {
588
- schemaObject = schemaObjectMapForRecord.get(field.name);
589
- }
590
- if (schemaObject) {
591
- return schemaObject;
592
- } else {
593
- const rawValue = editable ? cache.getAttr(identifier, path) : cache.getRemoteAttr(identifier, path);
594
- if (!rawValue) {
595
- return null;
994
+ function setObjectField(context) {
995
+ const {
996
+ field,
997
+ value,
998
+ record
999
+ } = context;
1000
+ const {
1001
+ cache,
1002
+ schema
1003
+ } = context.store;
1004
+ if (!field.type) {
1005
+ let newValue = value;
1006
+ if (value !== null) {
1007
+ newValue = {
1008
+ ...value
1009
+ };
1010
+ } else {
1011
+ ManagedObjectMap.delete(record);
596
1012
  }
597
- const embeddedPath = path.slice();
598
- schemaObject = new ReactiveResource(store, identifier, {
599
- [Editable]: editable,
600
- [Legacy]: legacy
601
- }, true, field, embeddedPath);
602
- }
603
- if (!schemaObjectMapForRecord) {
604
- ManagedObjectMap.set(record, new Map([[field.name, schemaObject]]));
605
- } else {
606
- schemaObjectMapForRecord.set(field.name, schemaObject);
1013
+ cache.setAttr(context.resourceKey, context.path, newValue);
1014
+ const peeked = peekManagedObject(record, field);
1015
+ if (peeked) {
1016
+ const objSignal = peeked[OBJECT_SIGNAL];
1017
+ objSignal.isStale = true;
1018
+ }
1019
+ return true;
607
1020
  }
608
- return schemaObject;
609
- }
610
- function computeAttribute(cache, identifier, prop, editable) {
611
- return editable ? cache.getAttr(identifier, prop) : cache.getRemoteAttr(identifier, prop);
612
- }
613
- function computeDerivation(schema, record, identifier, field, prop) {
614
- return schema.derivation(field)(record, field.options ?? null, prop);
1021
+ const transform = schema.transformation(field);
1022
+ const rawValue = transform.serialize({
1023
+ ...value
1024
+ }, field.options ?? null, record);
1025
+ cache.setAttr(context.resourceKey, context.path, rawValue);
1026
+ const peeked = peekManagedObject(record, field);
1027
+ if (peeked) {
1028
+ const objSignal = peeked[OBJECT_SIGNAL];
1029
+ objSignal.isStale = true;
1030
+ }
1031
+ return true;
615
1032
  }
1033
+
616
1034
  // TODO probably this should just be a Document
617
1035
  // but its separate until we work out the lid situation
618
1036
  class ResourceRelationship {
619
- constructor(store, cache, parent, identifier, field, name, editable) {
620
- const rawValue = editable ? cache.getRelationship(identifier, name) : cache.getRemoteRelationship(identifier, name);
1037
+ constructor(context) {
1038
+ const {
1039
+ store,
1040
+ resourceKey
1041
+ } = context;
1042
+ const {
1043
+ cache
1044
+ } = store;
1045
+ const name = getFieldCacheKeyStrict(context.field);
1046
+ const rawValue = context.editable ? cache.getRelationship(resourceKey, name) : cache.getRemoteRelationship(resourceKey, name);
621
1047
 
622
1048
  // TODO setup true lids for relationship documents
623
1049
  // @ts-expect-error we need to give relationship documents a lid
624
1050
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
625
- this.lid = rawValue.lid ?? rawValue.links?.self ?? `relationship:${identifier.lid}.${name}`;
1051
+ this.lid = rawValue.lid ?? rawValue.links?.self ?? `relationship:${resourceKey.lid}.${name}`;
626
1052
  this.data = rawValue.data ? store.peekRecord(rawValue.data) : null;
627
1053
  this.name = name;
628
1054
  if (macroCondition(getGlobalConfig().WarpDrive.env.DEBUG)) {
@@ -633,7 +1059,7 @@ class ResourceRelationship {
633
1059
  this.meta = rawValue.meta ?? {};
634
1060
  }
635
1061
  this[RecordStore] = store;
636
- this[Parent] = parent;
1062
+ this[Parent] = context.record;
637
1063
  }
638
1064
  fetch(options) {
639
1065
  const url = options?.url ?? getHref(this.links.related) ?? getHref(this.links.self) ?? null;
@@ -659,60 +1085,272 @@ function getHref(link) {
659
1085
  }
660
1086
  return link.href;
661
1087
  }
662
- function computeResource(store, cache, parent, identifier, field, prop, editable) {
663
- if (field.kind !== 'resource') {
664
- throw new Error(`The schema for ${identifier.type}.${String(prop)} is not a resource relationship`);
1088
+ function getResourceField(context) {
1089
+ entangleSignal(context.signals, context.record, context.path.at(-1), null);
1090
+ return new ResourceRelationship(context);
1091
+ }
1092
+ function setResourceField(context) {
1093
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1094
+ {
1095
+ throw new Error(`setting resource relationships is not yet supported`);
1096
+ }
1097
+ })() : {};
1098
+ return false;
1099
+ }
1100
+ function setSchemaArrayField(context) {
1101
+ const arrayValue = context.value?.slice();
1102
+ const fieldSignal = peekInternalSignal(context.signals, context.path.at(-1));
1103
+ const peeked = fieldSignal?.value;
1104
+ context.store.cache.setAttr(context.resourceKey, context.path, arrayValue);
1105
+ if (peeked) {
1106
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1107
+ if (!test) {
1108
+ throw new Error(`Expected the peekManagedArray for ${context.field.kind} to return a ManagedArray`);
1109
+ }
1110
+ })(ARRAY_SIGNAL in peeked) : {};
1111
+ const arrSignal = peeked[ARRAY_SIGNAL];
1112
+ arrSignal.isStale = true;
1113
+ if (!Array.isArray(arrayValue)) {
1114
+ fieldSignal.value = null;
1115
+ }
665
1116
  }
666
- return new ResourceRelationship(store, cache, parent, identifier, field, prop, editable);
1117
+ return true;
667
1118
  }
668
- function computeHasMany(store, schema, cache, record, identifier, field, path, editable, legacy) {
669
- // the thing we hand out needs to know its owner and path in a private manner
670
- // its "address" is the parent identifier (identifier) + field name (field.name)
671
- // in the nested object case field name here is the full dot path from root resource to this value
672
- // its "key" is the field on the parent record
673
- // its "owner" is the parent record
674
-
675
- const managedArrayMapForRecord = ManagedArrayMap.get(record);
676
- let managedArray;
677
- if (managedArrayMapForRecord) {
678
- managedArray = managedArrayMapForRecord.get(field.name);
1119
+ function getSchemaObjectField(context) {
1120
+ const signal = entangleSignal(context.signals, context.record, context.path.at(-1), null);
1121
+ const {
1122
+ store,
1123
+ resourceKey,
1124
+ path
1125
+ } = context;
1126
+ const {
1127
+ cache
1128
+ } = store;
1129
+ const rawValue = context.editable ? cache.getAttr(resourceKey, path) : cache.getRemoteAttr(resourceKey, path);
1130
+ if (!rawValue) {
1131
+ if (signal.value) {
1132
+ const value = signal.value;
1133
+ // TODO if we had idle scheduling this should be done there.
1134
+ void Promise.resolve().then(() => {
1135
+ value.value[Destroy]();
1136
+ });
1137
+ signal.value = null;
1138
+ }
1139
+ return null;
679
1140
  }
680
- if (managedArray) {
681
- return managedArray;
682
- } else {
683
- const rawValue = cache.getRelationship(identifier, field.name);
684
- if (!rawValue) {
685
- return null;
1141
+ const {
1142
+ field
1143
+ } = context;
1144
+ const {
1145
+ schema
1146
+ } = store;
1147
+ let objectType;
1148
+ if (field.options?.polymorphic) {
1149
+ const typePath = field.options.type ?? 'type';
1150
+ // if we are polymorphic, then context.field.options.type will
1151
+ // either specify a path on the rawValue to use as the type, defaulting to "type" or
1152
+ // the special string "@hash" which tells us to treat field.type as a hashFn name with which
1153
+ // to calc the type.
1154
+ if (typePath === '@hash') {
1155
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1156
+ if (!test) {
1157
+ throw new Error(`Expected the field to define a hashFn as its type`);
1158
+ }
1159
+ })(field.type) : {};
1160
+ const hashFn = schema.hashFn({
1161
+ type: field.type
1162
+ });
1163
+ // TODO consider if there are better options and name args we could provide.
1164
+ objectType = hashFn(rawValue, null, null);
1165
+ } else {
1166
+ objectType = rawValue[typePath];
1167
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1168
+ if (!test) {
1169
+ throw new Error(`Expected the type path for the field to be a value on the raw object`);
1170
+ }
1171
+ })(typePath && objectType && typeof objectType === 'string') : {};
686
1172
  }
687
- managedArray = new RelatedCollection({
688
- store,
689
- type: field.type,
690
- identifier,
691
- cache,
692
- field: legacy ? field : undefined,
693
- // we divorce the reference here because ManyArray mutates the target directly
694
- // before sending the mutation op to the cache. We may be able to avoid this in the future
695
- identifiers: rawValue.data?.slice(),
696
- key: field.name,
697
- meta: rawValue.meta || null,
698
- links: rawValue.links || null,
699
- isPolymorphic: field.options.polymorphic ?? false,
700
- isAsync: field.options.async ?? false,
701
- // TODO: Grab the proper value
702
- _inverseIsAsync: false,
703
- // @ts-expect-error Typescript doesn't have a way for us to thread the generic backwards so it infers unknown instead of T
704
- manager: new ManyArrayManager(record, editable),
705
- isLoaded: true,
706
- allowMutation: editable
707
- });
708
- if (!managedArrayMapForRecord) {
709
- ManagedArrayMap.set(record, new Map([[field.name, managedArray]]));
1173
+ } else {
1174
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1175
+ if (!test) {
1176
+ throw new Error(`A non-polymorphic SchemaObjectField must provide a SchemaObject type in its definition`);
1177
+ }
1178
+ })(field.type) : {};
1179
+ objectType = field.type;
1180
+ }
1181
+ const hashField = schema.resource({
1182
+ type: objectType
1183
+ }).identity;
1184
+ const identity = hashField ? schema.hashFn(hashField)(rawValue, hashField.options ?? null, hashField.name) : field.name;
1185
+ const cachedSchemaObject = signal.value;
1186
+ if (cachedSchemaObject) {
1187
+ if (cachedSchemaObject.type === objectType && cachedSchemaObject.identity === identity) {
1188
+ return cachedSchemaObject.value;
710
1189
  } else {
711
- managedArrayMapForRecord.set(field.name, managedArray);
1190
+ // TODO if we had idle scheduling this should be done there.
1191
+ void Promise.resolve().then(() => {
1192
+ cachedSchemaObject.value[Destroy]();
1193
+ });
712
1194
  }
713
1195
  }
714
- return managedArray;
1196
+ const schemaObject = new ReactiveResource({
1197
+ store: context.store,
1198
+ resourceKey: context.resourceKey,
1199
+ modeName: context.modeName,
1200
+ legacy: context.legacy,
1201
+ editable: context.editable,
1202
+ path: context.path,
1203
+ field: context.field,
1204
+ value: objectType
1205
+ });
1206
+ signal.value = {
1207
+ type: objectType,
1208
+ identity: identity,
1209
+ value: schemaObject
1210
+ };
1211
+ return schemaObject;
1212
+ }
1213
+ function setSchemaObjectField(context) {
1214
+ const {
1215
+ store,
1216
+ value
1217
+ } = context;
1218
+ let newValue = value;
1219
+ if (value !== null) {
1220
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1221
+ if (!test) {
1222
+ throw new Error(`Expected value to be an object`);
1223
+ }
1224
+ })(typeof value === 'object') : {};
1225
+ newValue = {
1226
+ ...value
1227
+ };
1228
+ const schemaFields = store.schema.fields({
1229
+ type: context.field.type
1230
+ });
1231
+ for (const key of Object.keys(newValue)) {
1232
+ if (!schemaFields.has(key)) {
1233
+ throw new Error(`Field ${key} does not exist on schema object ${context.field.type}`);
1234
+ }
1235
+ }
1236
+ } else {
1237
+ ManagedObjectMap.delete(context.record);
1238
+ }
1239
+ store.cache.setAttr(context.resourceKey, context.path, newValue);
1240
+ // const peeked = peekManagedObject(self, field);
1241
+ // if (peeked) {
1242
+ // const objSignal = peeked[OBJECT_SIGNAL];
1243
+ // objSignal.isStale = true;
1244
+ // }
1245
+ return true;
715
1246
  }
1247
+ const DefaultMode = {
1248
+ '@hash': {
1249
+ get: getHashField,
1250
+ set: setHashField,
1251
+ mutable: false,
1252
+ enumerable: false,
1253
+ serializable: false
1254
+ },
1255
+ '@id': {
1256
+ get: getIdentityField,
1257
+ set: setIdentityField,
1258
+ mutable: true,
1259
+ enumerable: true,
1260
+ serializable: true
1261
+ },
1262
+ '@local': {
1263
+ get: getLocalField,
1264
+ set: setLocalField,
1265
+ mutable: true,
1266
+ enumerable: false,
1267
+ serializable: false
1268
+ },
1269
+ alias: {
1270
+ get: getAliasField,
1271
+ set: setAliasField,
1272
+ mutable: true,
1273
+ enumerable: true,
1274
+ serializable: false
1275
+ },
1276
+ array: {
1277
+ get: getArrayField,
1278
+ set: setArrayField,
1279
+ mutable: true,
1280
+ enumerable: true,
1281
+ serializable: true
1282
+ },
1283
+ attribute: {
1284
+ get: getAttributeField,
1285
+ set: setAttributeField,
1286
+ mutable: true,
1287
+ enumerable: true,
1288
+ serializable: true
1289
+ },
1290
+ belongsTo: {
1291
+ get: getBelongsToField,
1292
+ set: setBelongsToField,
1293
+ mutable: true,
1294
+ enumerable: true,
1295
+ serializable: true
1296
+ },
1297
+ collection: {
1298
+ get: getCollectionField,
1299
+ set: setCollectionField,
1300
+ mutable: true,
1301
+ enumerable: true,
1302
+ serializable: true
1303
+ },
1304
+ derived: {
1305
+ get: getDerivedField,
1306
+ set: setDerivedField,
1307
+ mutable: true,
1308
+ enumerable: true,
1309
+ serializable: false
1310
+ },
1311
+ field: {
1312
+ get: getGenericField,
1313
+ set: setGenericField,
1314
+ mutable: true,
1315
+ enumerable: true,
1316
+ serializable: true
1317
+ },
1318
+ hasMany: {
1319
+ get: getHasManyField,
1320
+ set: setHasManyField,
1321
+ mutable: true,
1322
+ enumerable: true,
1323
+ serializable: true
1324
+ },
1325
+ object: {
1326
+ get: getObjectField,
1327
+ set: setObjectField,
1328
+ mutable: true,
1329
+ enumerable: true,
1330
+ serializable: true
1331
+ },
1332
+ resource: {
1333
+ get: getResourceField,
1334
+ set: setResourceField,
1335
+ mutable: true,
1336
+ enumerable: true,
1337
+ serializable: true
1338
+ },
1339
+ 'schema-array': {
1340
+ get: getArrayField,
1341
+ set: setSchemaArrayField,
1342
+ mutable: true,
1343
+ enumerable: true,
1344
+ serializable: true
1345
+ },
1346
+ 'schema-object': {
1347
+ get: getSchemaObjectField,
1348
+ set: setSchemaObjectField,
1349
+ mutable: true,
1350
+ enumerable: true,
1351
+ serializable: true
1352
+ }
1353
+ };
716
1354
  const IgnoredGlobalFields = new Set(['length', 'nodeType', 'then', 'setInterval', 'document', STRUCTURED]);
717
1355
  const symbolList = [Destroy, RecordStore, Identifier, Editable, Parent, Checkout, Legacy, EmbeddedPath, EmbeddedField];
718
1356
  const RecordSymbols = new Set(symbolList);
@@ -737,27 +1375,41 @@ const Editables = new WeakMap();
737
1375
  */
738
1376
  // eslint-disable-next-line @typescript-eslint/no-extraneous-class
739
1377
  class ReactiveResource {
740
- constructor(store, identifier, Mode, isEmbedded = false, embeddedField = null, embeddedPath = null) {
741
- // eslint-disable-next-line @typescript-eslint/no-this-alias
742
- const self = this;
1378
+ constructor(context) {
1379
+ const {
1380
+ store
1381
+ } = context;
1382
+ const identifier = context.resourceKey;
1383
+ const embeddedField = context.field;
1384
+ const embeddedPath = context.path;
1385
+ const isEmbedded = context.field !== null;
743
1386
  this[RecordStore] = store;
744
1387
  if (isEmbedded) {
745
1388
  this[Parent] = identifier;
746
1389
  } else {
747
1390
  this[Identifier] = identifier;
748
1391
  }
749
- const IS_EDITABLE = this[Editable] = Mode[Editable] ?? false;
750
- this[Legacy] = Mode[Legacy] ?? false;
1392
+ const IS_EDITABLE = this[Editable] = context.editable ?? false;
751
1393
  const schema = store.schema;
752
- const cache = store.cache;
753
- const identityField = schema.resource(isEmbedded ? embeddedField : identifier).identity;
1394
+ this[Legacy] = context.legacy ?? false;
1395
+ const objectType = isEmbedded ? context.value : identifier.type;
1396
+ const ResourceSchema = schema.resource(isEmbedded ? {
1397
+ type: objectType
1398
+ } : identifier);
1399
+ const identityField = ResourceSchema.identity;
754
1400
  const BoundFns = new Map();
755
1401
 
756
1402
  // prettier-ignore
757
- const extensions = !Mode[Legacy] ? null : isEmbedded ? schema.CAUTION_MEGA_DANGER_ZONE_objectExtensions(embeddedField) : schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(identifier);
1403
+ const extensions = !context.legacy ? null : isEmbedded ? schema.CAUTION_MEGA_DANGER_ZONE_objectExtensions(embeddedField, objectType) : schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(identifier);
758
1404
  this[EmbeddedField] = embeddedField;
759
1405
  this[EmbeddedPath] = embeddedPath;
760
- const fields = isEmbedded ? schema.fields(embeddedField) : schema.fields(identifier);
1406
+ const fields = isEmbedded ? schema.fields({
1407
+ type: objectType
1408
+ }) : schema.fields(identifier);
1409
+ const method = typeof schema.cacheFields === 'function' ? 'cacheFields' : 'fields';
1410
+ const cacheFields = isEmbedded ? schema[method]({
1411
+ type: objectType
1412
+ }) : schema[method](identifier);
761
1413
  const signals = withSignalStore(this);
762
1414
  const proxy = new Proxy(this, {
763
1415
  ownKeys() {
@@ -934,74 +1586,64 @@ class ReactiveResource {
934
1586
  throw new Error(`Alias fields cannot alias '@id' '@local' '@hash' or 'derived' fields`);
935
1587
  }
936
1588
  })(maybeField.kind !== 'alias' || !['@id', '@local', '@hash', 'derived'].includes(maybeField.options.kind)) : {};
1589
+ /**
1590
+ * Prop Array is the path from a resource to the field including
1591
+ * intermediate "links" on arrays,objects,schema-arrays and schema-objects.
1592
+ *
1593
+ * E.g. in the following
1594
+ *
1595
+ * ```
1596
+ * const user = {
1597
+ * addresses: [{
1598
+ * street: 'Sunset Blvd',
1599
+ * zip: 90210
1600
+ * }]
1601
+ * }
1602
+ * ```
1603
+ *
1604
+ * The propArray for "street" is ['addresses', 0, 'street']
1605
+ *
1606
+ * Prop Array follows the `cache` path to the value, not the ui path.
1607
+ * Thus, if `addresses` has a sourceKey of `user_addresses` and
1608
+ * `zip` has a sourceKey of `zip_code` then the propArray for "zip" is
1609
+ * ['user_addresses', 0, 'zip_code']
1610
+ */
937
1611
  const propArray = isEmbedded ? embeddedPath.slice() : [];
938
1612
  // we use the field.name instead of prop here because we want to use the cache-path not
939
1613
  // the record path.
940
- propArray.push(field.name);
941
- // propArray.push(prop as string);
942
-
1614
+ // SAFETY: we lie as string here because if we were to get null
1615
+ // we would be in a field kind that won't use the propArray below.
1616
+ const fieldCacheKey = getFieldCacheKey(field);
1617
+ propArray.push(fieldCacheKey);
943
1618
  switch (field.kind) {
944
1619
  case '@id':
945
- entangleSignal(signals, receiver, '@identity', null);
946
- return identifier.id;
947
1620
  case '@hash':
948
- // TODO pass actual cache value not {}
949
- return schema.hashFn(field)({}, field.options ?? null, field.name ?? null);
950
1621
  case '@local':
951
- {
952
- return computeLocal(receiver, field, prop);
953
- }
1622
+ case 'derived':
954
1623
  case 'field':
955
- entangleSignal(signals, receiver, field.name, null);
956
- return computeField(schema, cache, target, identifier, field, propArray, IS_EDITABLE);
957
1624
  case 'attribute':
958
- entangleSignal(signals, receiver, field.name, null);
959
- return computeAttribute(cache, identifier, prop, IS_EDITABLE);
960
- case 'resource':
961
- entangleSignal(signals, receiver, field.name, null);
962
- return computeResource(store, cache, target, identifier, field, prop, IS_EDITABLE);
963
- case 'derived':
964
- return computeDerivation(schema, receiver, identifier, field, prop);
965
1625
  case 'schema-array':
966
1626
  case 'array':
967
- entangleSignal(signals, receiver, field.name, null);
968
- return computeArray(store, schema, cache, target, identifier, field, propArray, Mode[Editable], Mode[Legacy]);
969
- case 'object':
970
- entangleSignal(signals, receiver, field.name, null);
971
- return computeObject(schema, cache, target, identifier, field, propArray, Mode[Editable], Mode[Legacy]);
972
1627
  case 'schema-object':
973
- entangleSignal(signals, receiver, field.name, null);
974
- // run transform, then use that value as the object to manage
975
- return computeSchemaObject(store, cache, target, identifier, field, propArray, Mode[Legacy], Mode[Editable]);
1628
+ case 'object':
1629
+ case 'resource':
976
1630
  case 'belongsTo':
977
- if (field.options.linksMode) {
978
- entangleSignal(signals, receiver, field.name, null);
979
- const rawValue = IS_EDITABLE ? cache.getRelationship(identifier, field.name) : cache.getRemoteRelationship(identifier, field.name);
980
-
981
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
982
- return rawValue.data ? store.peekRecord(rawValue.data) : null;
983
- }
984
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
985
- if (!test) {
986
- throw new Error(`Can only use belongsTo fields when the resource is in legacy mode`);
987
- }
988
- })(Mode[Legacy]) : {};
989
- entangleSignal(signals, receiver, field.name, null);
990
- return schema._kind('@legacy', 'belongsTo').get(store, receiver, identifier, field);
991
1631
  case 'hasMany':
992
- if (field.options.linksMode) {
993
- entangleSignal(signals, receiver, field.name, null);
994
- return computeHasMany(store, schema, cache, target, identifier, field, propArray, Mode[Editable], Mode[Legacy]);
995
- }
996
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
997
- if (!test) {
998
- throw new Error(`Can only use hasMany fields when the resource is in legacy mode`);
999
- }
1000
- })(Mode[Legacy]) : {};
1001
- entangleSignal(signals, receiver, field.name, null);
1002
- return schema._kind('@legacy', 'hasMany').get(store, receiver, identifier, field);
1632
+ case 'collection':
1633
+ return DefaultMode[field.kind].get({
1634
+ store,
1635
+ resourceKey: identifier,
1636
+ modeName: context.modeName,
1637
+ legacy: context.legacy,
1638
+ editable: context.editable,
1639
+ path: propArray,
1640
+ field: field,
1641
+ record: receiver,
1642
+ signals,
1643
+ value: null
1644
+ });
1003
1645
  default:
1004
- throw new Error(`Field '${String(prop)}' on '${identifier.type}' has the unknown kind '${field.kind}'`);
1646
+ assertNeverField(identifier, field, propArray);
1005
1647
  }
1006
1648
  },
1007
1649
  set(target, prop, value, receiver) {
@@ -1028,203 +1670,64 @@ class ReactiveResource {
1028
1670
  throw new Error(`Alias fields cannot alias '@id' '@local' '@hash' or 'derived' fields`);
1029
1671
  }
1030
1672
  })(maybeField.kind !== 'alias' || !['@id', '@local', '@hash', 'derived'].includes(maybeField.options.kind)) : {};
1673
+ /**
1674
+ * Prop Array is the path from a resource to the field including
1675
+ * intermediate "links" on arrays,objects,schema-arrays and schema-objects.
1676
+ *
1677
+ * E.g. in the following
1678
+ *
1679
+ * ```
1680
+ * const user = {
1681
+ * addresses: [{
1682
+ * street: 'Sunset Blvd',
1683
+ * zip: 90210
1684
+ * }]
1685
+ * }
1686
+ * ```
1687
+ *
1688
+ * The propArray for "street" is ['addresses', 0, 'street']
1689
+ *
1690
+ * Prop Array follows the `cache` path to the value, not the ui path.
1691
+ * Thus, if `addresses` has a sourceKey of `user_addresses` and
1692
+ * `zip` has a sourceKey of `zip_code` then the propArray for "zip" is
1693
+ * ['user_addresses', 0, 'zip_code']
1694
+ */
1031
1695
  const propArray = isEmbedded ? embeddedPath.slice() : [];
1032
1696
  // we use the field.name instead of prop here because we want to use the cache-path not
1033
1697
  // the record path.
1034
- propArray.push(field.name);
1035
- // propArray.push(prop as string);
1036
-
1698
+ // SAFETY: we lie as string here because if we were to get null
1699
+ // we would be in a field kind that won't use the propArray below.
1700
+ const fieldCacheKey = getFieldCacheKey(field);
1701
+ propArray.push(fieldCacheKey);
1037
1702
  switch (field.kind) {
1038
1703
  case '@id':
1039
- {
1040
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1041
- if (!test) {
1042
- throw new Error(`Expected to receive a string id`);
1043
- }
1044
- })(typeof value === 'string' && value.length) : {};
1045
- const normalizedId = String(value);
1046
- const didChange = normalizedId !== identifier.id;
1047
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1048
- if (!test) {
1049
- throw new Error(`Cannot set ${identifier.type} record's id to ${normalizedId}, because id is already ${identifier.id}`);
1050
- }
1051
- })(!didChange || identifier.id === null) : {};
1052
- if (normalizedId !== null && didChange) {
1053
- store._instanceCache.setRecordId(identifier, normalizedId);
1054
- store.notifications.notify(identifier, 'identity');
1055
- }
1056
- return true;
1057
- }
1704
+ case '@hash':
1058
1705
  case '@local':
1059
- {
1060
- const signal = getOrCreateInternalSignal(signals, receiver, prop, field.options?.defaultValue ?? null);
1061
- if (signal.value !== value) {
1062
- signal.value = value;
1063
- notifyInternalSignal(signal);
1064
- }
1065
- return true;
1066
- }
1067
1706
  case 'field':
1068
- {
1069
- if (!field.type) {
1070
- cache.setAttr(identifier, propArray, value);
1071
- return true;
1072
- }
1073
- const transform = schema.transformation(field);
1074
- const rawValue = transform.serialize(value, field.options ?? null, target);
1075
- cache.setAttr(identifier, propArray, rawValue);
1076
- return true;
1077
- }
1078
1707
  case 'attribute':
1079
- {
1080
- cache.setAttr(identifier, propArray, value);
1081
- return true;
1082
- }
1708
+ case 'derived':
1083
1709
  case 'array':
1084
- {
1085
- if (!field.type) {
1086
- cache.setAttr(identifier, propArray, value?.slice());
1087
- const peeked = peekManagedArray(self, field);
1088
- if (peeked) {
1089
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1090
- if (!test) {
1091
- throw new Error(`Expected the peekManagedArray for ${field.kind} to return a ManagedArray`);
1092
- }
1093
- })(ARRAY_SIGNAL in peeked) : {};
1094
- const arrSignal = peeked[ARRAY_SIGNAL];
1095
- arrSignal.isStale = true;
1096
- }
1097
- if (!Array.isArray(value)) {
1098
- ManagedArrayMap.delete(target);
1099
- }
1100
- return true;
1101
- }
1102
- const transform = schema.transformation(field);
1103
- const rawValue = value.map(item => transform.serialize(item, field.options ?? null, target));
1104
- cache.setAttr(identifier, propArray, rawValue);
1105
- const peeked = peekManagedArray(self, field);
1106
- if (peeked) {
1107
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1108
- if (!test) {
1109
- throw new Error(`Expected the peekManagedArray for ${field.kind} to return a ManagedArray`);
1110
- }
1111
- })(ARRAY_SIGNAL in peeked) : {};
1112
- const arrSignal = peeked[ARRAY_SIGNAL];
1113
- arrSignal.isStale = true;
1114
- }
1115
- return true;
1116
- }
1117
1710
  case 'schema-array':
1118
- {
1119
- const arrayValue = value?.slice();
1120
- if (!Array.isArray(arrayValue)) {
1121
- ManagedArrayMap.delete(target);
1122
- }
1123
- cache.setAttr(identifier, propArray, arrayValue);
1124
- const peeked = peekManagedArray(self, field);
1125
- if (peeked) {
1126
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1127
- if (!test) {
1128
- throw new Error(`Expected the peekManagedArray for ${field.kind} to return a ManagedArray`);
1129
- }
1130
- })(ARRAY_SIGNAL in peeked) : {};
1131
- const arrSignal = peeked[ARRAY_SIGNAL];
1132
- arrSignal.isStale = true;
1133
- }
1134
- if (!Array.isArray(value)) {
1135
- ManagedArrayMap.delete(target);
1136
- }
1137
- return true;
1138
- }
1139
- case 'object':
1140
- {
1141
- if (!field.type) {
1142
- let newValue = value;
1143
- if (value !== null) {
1144
- newValue = {
1145
- ...value
1146
- };
1147
- } else {
1148
- ManagedObjectMap.delete(target);
1149
- }
1150
- cache.setAttr(identifier, propArray, newValue);
1151
- const peeked = peekManagedObject(self, field);
1152
- if (peeked) {
1153
- const objSignal = peeked[OBJECT_SIGNAL];
1154
- objSignal.isStale = true;
1155
- }
1156
- return true;
1157
- }
1158
- const transform = schema.transformation(field);
1159
- const rawValue = transform.serialize({
1160
- ...value
1161
- }, field.options ?? null, target);
1162
- cache.setAttr(identifier, propArray, rawValue);
1163
- const peeked = peekManagedObject(self, field);
1164
- if (peeked) {
1165
- const objSignal = peeked[OBJECT_SIGNAL];
1166
- objSignal.isStale = true;
1167
- }
1168
- return true;
1169
- }
1170
1711
  case 'schema-object':
1171
- {
1172
- let newValue = value;
1173
- if (value !== null) {
1174
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1175
- if (!test) {
1176
- throw new Error(`Expected value to be an object`);
1177
- }
1178
- })(typeof value === 'object') : {};
1179
- newValue = {
1180
- ...value
1181
- };
1182
- const schemaFields = schema.fields({
1183
- type: field.type
1184
- });
1185
- for (const key of Object.keys(newValue)) {
1186
- if (!schemaFields.has(key)) {
1187
- throw new Error(`Field ${key} does not exist on schema object ${field.type}`);
1188
- }
1189
- }
1190
- } else {
1191
- ManagedObjectMap.delete(target);
1192
- }
1193
- cache.setAttr(identifier, propArray, newValue);
1194
- // const peeked = peekManagedObject(self, field);
1195
- // if (peeked) {
1196
- // const objSignal = peeked[OBJECT_SIGNAL];
1197
- // objSignal.isStale = true;
1198
- // }
1199
- return true;
1200
- }
1201
- case 'derived':
1202
- {
1203
- throw new Error(`Cannot set ${String(prop)} on ${identifier.type} because it is derived`);
1204
- }
1712
+ case 'object':
1713
+ case 'resource':
1205
1714
  case 'belongsTo':
1206
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1207
- if (!test) {
1208
- throw new Error(`Can only use belongsTo fields when the resource is in legacy mode`);
1209
- }
1210
- })(Mode[Legacy]) : {};
1211
- schema._kind('@legacy', 'belongsTo').set(store, receiver, identifier, field, value);
1212
- return true;
1213
1715
  case 'hasMany':
1214
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1215
- if (!test) {
1216
- throw new Error(`Can only use hasMany fields when the resource is in legacy mode`);
1217
- }
1218
- })(Mode[Legacy]) : {};
1219
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1220
- if (!test) {
1221
- throw new Error(`You must pass an array of records to set a hasMany relationship`);
1222
- }
1223
- })(Array.isArray(value)) : {};
1224
- schema._kind('@legacy', 'hasMany').set(store, receiver, identifier, field, value);
1225
- return true;
1716
+ case 'collection':
1717
+ return DefaultMode[field.kind].set({
1718
+ store,
1719
+ resourceKey: identifier,
1720
+ modeName: context.modeName,
1721
+ legacy: context.legacy,
1722
+ editable: context.editable,
1723
+ path: propArray,
1724
+ field: field,
1725
+ record: receiver,
1726
+ signals,
1727
+ value
1728
+ });
1226
1729
  default:
1227
- throw new Error(`Unknown field kind ${field.kind}`);
1730
+ return assertNeverField(identifier, field, propArray);
1228
1731
  }
1229
1732
  }
1230
1733
  });
@@ -1255,25 +1758,25 @@ class ReactiveResource {
1255
1758
  // TODO we should likely handle this notification here
1256
1759
  // also we should add a LOGGING flag
1257
1760
  // eslint-disable-next-line no-console
1258
- console.warn(`Notification unhandled for ${key.join(',')} on ${identifier.type}`, self);
1761
+ console.warn(`Notification unhandled for ${key.join(',')} on ${identifier.type}`, proxy);
1259
1762
  return;
1260
1763
  }
1261
1764
 
1262
1765
  // TODO we should add a LOGGING flag
1263
- // console.log(`Deep notification skipped for ${key.join('.')} on ${identifier.type}`, self);
1766
+ // console.log(`Deep notification skipped for ${key.join('.')} on ${identifier.type}`, proxy);
1264
1767
  // deep notify the key path
1265
1768
  } else {
1266
1769
  if (isEmbedded) return; // base paths never apply to embedded records
1267
1770
 
1268
1771
  // TODO determine what LOGGING flag to wrap this in if any
1269
- // console.log(`Notification for ${key} on ${identifier.type}`, self);
1772
+ // console.log(`Notification for ${key} on ${identifier.type}`, proxy);
1270
1773
  const signal = signals.get(key);
1271
1774
  if (signal) {
1272
1775
  notifyInternalSignal(signal);
1273
1776
  }
1274
- const field = fields.get(key);
1777
+ const field = cacheFields.get(key);
1275
1778
  if (field?.kind === 'array' || field?.kind === 'schema-array') {
1276
- const peeked = peekManagedArray(self, field);
1779
+ const peeked = signal?.value;
1277
1780
  if (peeked) {
1278
1781
  macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1279
1782
  if (!test) {
@@ -1285,7 +1788,7 @@ class ReactiveResource {
1285
1788
  }
1286
1789
  }
1287
1790
  if (field?.kind === 'object') {
1288
- const peeked = peekManagedObject(self, field);
1791
+ const peeked = peekManagedObject(proxy, field);
1289
1792
  if (peeked) {
1290
1793
  const objSignal = peeked[OBJECT_SIGNAL];
1291
1794
  notifyInternalSignal(objSignal);
@@ -1299,15 +1802,15 @@ class ReactiveResource {
1299
1802
  if (Array.isArray(key)) ;else {
1300
1803
  if (isEmbedded) return; // base paths never apply to embedded records
1301
1804
 
1302
- const field = fields.get(key);
1805
+ const field = cacheFields.get(key);
1303
1806
  macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1304
1807
  if (!test) {
1305
- throw new Error(`Expected relationshp ${key} to be the name of a field`);
1808
+ throw new Error(`Expected relationship ${key} to be the name of a field`);
1306
1809
  }
1307
1810
  })(field) : {};
1308
1811
  if (field.kind === 'belongsTo') {
1309
1812
  // TODO determine what LOGGING flag to wrap this in if any
1310
- // console.log(`Notification for ${key} on ${identifier.type}`, self);
1813
+ // console.log(`Notification for ${key} on ${identifier.type}`, proxy);
1311
1814
  const signal = signals.get(key);
1312
1815
  if (signal) {
1313
1816
  notifyInternalSignal(signal);
@@ -1315,9 +1818,12 @@ class ReactiveResource {
1315
1818
  // FIXME
1316
1819
  } else if (field.kind === 'resource') ;else if (field.kind === 'hasMany') {
1317
1820
  if (field.options.linksMode) {
1318
- const peeked = peekManagedArray(self, field);
1319
- if (peeked) {
1320
- notifyInternalSignal(peeked[ARRAY_SIGNAL]);
1821
+ const signal = signals.get(key);
1822
+ if (signal) {
1823
+ const peeked = signal.value;
1824
+ if (peeked) {
1825
+ notifyInternalSignal(peeked[ARRAY_SIGNAL]);
1826
+ }
1321
1827
  }
1322
1828
  return;
1323
1829
  }
@@ -1325,7 +1831,7 @@ class ReactiveResource {
1325
1831
  if (!test) {
1326
1832
  throw new Error(`Can only use hasMany fields when the resource is in legacy mode`);
1327
1833
  }
1328
- })(Mode[Legacy]) : {};
1834
+ })(context.legacy) : {};
1329
1835
  if (schema._kind('@legacy', 'hasMany').notify(store, proxy, identifier, field)) {
1330
1836
  macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1331
1837
  if (!test) {
@@ -1381,10 +1887,17 @@ function _CHECKOUT(record) {
1381
1887
  if (isEmbedded) {
1382
1888
  throw new Error(`Cannot checkout an embedded record (yet)`);
1383
1889
  }
1384
- const editableRecord = new ReactiveResource(record[RecordStore], record[Identifier], {
1385
- [Editable]: true,
1386
- [Legacy]: record[Legacy]
1387
- }, isEmbedded, embeddedType, embeddedPath);
1890
+ const legacy = record[Legacy];
1891
+ const editableRecord = new ReactiveResource({
1892
+ store: record[RecordStore],
1893
+ resourceKey: record[Identifier],
1894
+ modeName: legacy ? 'legacy' : 'polaris',
1895
+ legacy: legacy,
1896
+ editable: true,
1897
+ path: null,
1898
+ field: null,
1899
+ value: null
1900
+ });
1388
1901
  setRecordIdentifier(editableRecord, recordIdentifierFor(record));
1389
1902
  return Promise.resolve(editableRecord);
1390
1903
  }
@@ -1396,6 +1909,17 @@ function _DESTROY(record) {
1396
1909
  record.isDestroyed = true;
1397
1910
  }
1398
1911
  record[RecordStore].notifications.unsubscribe(record.___notifications);
1912
+
1913
+ // FIXME we need a way to also unsubscribe all SchemaObjects when the primary
1914
+ // resource is destroyed.
1915
+ }
1916
+ function assertNeverField(identifier, field, path) {
1917
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1918
+ {
1919
+ throw new Error(`Cannot use unknown field kind ${field.kind} on <${identifier.type}>.${Array.isArray(path) ? path.join('.') : path}`);
1920
+ }
1921
+ })() : {};
1922
+ return false;
1399
1923
  }
1400
1924
  function instantiateRecord(store, identifier, createArgs) {
1401
1925
  const schema = store.schema;
@@ -1405,11 +1929,17 @@ function instantiateRecord(store, identifier, createArgs) {
1405
1929
  throw new Error(`Expected a resource schema`);
1406
1930
  }
1407
1931
  })(isResourceSchema(resourceSchema)) : {};
1408
- const isLegacy = resourceSchema?.legacy ?? false;
1409
- const isEditable = isLegacy || store.cache.isNew(identifier);
1410
- const record = new ReactiveResource(store, identifier, {
1411
- [Editable]: isEditable,
1412
- [Legacy]: isLegacy
1932
+ const legacy = resourceSchema?.legacy ?? false;
1933
+ const editable = legacy || store.cache.isNew(identifier);
1934
+ const record = new ReactiveResource({
1935
+ store,
1936
+ resourceKey: identifier,
1937
+ modeName: legacy ? 'legacy' : 'polaris',
1938
+ legacy: legacy,
1939
+ editable: editable,
1940
+ path: null,
1941
+ field: null,
1942
+ value: null
1413
1943
  });
1414
1944
  if (createArgs) {
1415
1945
  Object.assign(record, createArgs);
@@ -1546,7 +2076,7 @@ function getExt(extCache, type, extName) {
1546
2076
  function hasObjectSchema(field) {
1547
2077
  return 'kind' in field && (field.kind === 'schema-array' || field.kind === 'schema-object');
1548
2078
  }
1549
- function processExtensions(schema, field, scenario) {
2079
+ function processExtensions(schema, field, scenario, resolvedType) {
1550
2080
  // if we're looking up extensions for a resource, there is no
1551
2081
  // merging required so if we have no objectExtensions
1552
2082
  // we are done.
@@ -1573,12 +2103,16 @@ function processExtensions(schema, field, scenario) {
1573
2103
  if (!hasObjectSchema(field)) {
1574
2104
  return null;
1575
2105
  }
1576
- return schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(field);
2106
+ return schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(resolvedType ? {
2107
+ type: resolvedType
2108
+ } : field);
1577
2109
  }
1578
2110
 
1579
2111
  // if we have made it here, we have extensions, lets check if there's
1580
2112
  // a cached version we can use
1581
- const baseExtensions = scenario === 'resource' && hasObjectSchema(field) ? schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(field) : scenario === 'object' && hasObjectSchema(field) ? schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(field) : null;
2113
+ const baseExtensions = scenario === 'resource' && hasObjectSchema(field) ? schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(field) : scenario === 'object' && hasObjectSchema(field) ? schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(resolvedType ? {
2114
+ type: resolvedType
2115
+ } : field) : null;
1582
2116
  if (!baseExtensions && extensions.length === 1) {
1583
2117
  const value = getExt(extCache, type, extensions[0]);
1584
2118
  fieldCache[type].set(field, value);
@@ -1836,16 +2370,21 @@ class SchemaService {
1836
2370
  relationships[field.name] = field;
1837
2371
  }
1838
2372
  }
2373
+ const cacheFields = null;
1839
2374
  const traits = new Set(isResourceSchema(schema) ? schema.traits : []);
1840
2375
  const finalized = traits.size === 0;
1841
2376
  const internalSchema = {
1842
2377
  original: schema,
1843
2378
  finalized,
1844
2379
  fields,
2380
+ cacheFields,
1845
2381
  relationships,
1846
2382
  attributes,
1847
2383
  traits
1848
2384
  };
2385
+ if (traits.size === 0) {
2386
+ internalSchema.cacheFields = getCacheFields(internalSchema);
2387
+ }
1849
2388
  this._schemas.set(schema.type, internalSchema);
1850
2389
  }
1851
2390
 
@@ -1902,13 +2441,13 @@ class SchemaService {
1902
2441
  }
1903
2442
  CAUTION_MEGA_DANGER_ZONE_resourceExtensions(resource) {
1904
2443
  const schema = this.resource(resource);
1905
- return processExtensions(this, schema, 'resource');
2444
+ return processExtensions(this, schema, 'resource', null);
1906
2445
  }
1907
- CAUTION_MEGA_DANGER_ZONE_objectExtensions(field) {
1908
- return processExtensions(this, field, 'object');
2446
+ CAUTION_MEGA_DANGER_ZONE_objectExtensions(field, resolvedType) {
2447
+ return processExtensions(this, field, 'object', resolvedType);
1909
2448
  }
1910
2449
  CAUTION_MEGA_DANGER_ZONE_arrayExtensions(field) {
1911
- return processExtensions(this, field, 'array');
2450
+ return processExtensions(this, field, 'array', null);
1912
2451
  }
1913
2452
  CAUTION_MEGA_DANGER_ZONE_hasExtension(ext) {
1914
2453
  return this._extensions[ext.kind].has(ext.name);
@@ -1976,6 +2515,20 @@ class SchemaService {
1976
2515
  }
1977
2516
  return schema.fields;
1978
2517
  }
2518
+ cacheFields({
2519
+ type
2520
+ }) {
2521
+ const schema = this._schemas.get(type);
2522
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
2523
+ if (!test) {
2524
+ throw new Error(`No schema defined for ${type}`);
2525
+ }
2526
+ })(schema) : {};
2527
+ if (!schema.finalized) {
2528
+ finalizeResource(this, schema);
2529
+ }
2530
+ return schema.cacheFields;
2531
+ }
1979
2532
  hasResource(resource) {
1980
2533
  return this._schemas.has(resource.type);
1981
2534
  }
@@ -2064,8 +2617,27 @@ function finalizeResource(schema, resource) {
2064
2617
  }
2065
2618
  mergeMap(fields, resource.fields);
2066
2619
  resource.fields = fields;
2620
+ resource.cacheFields = getCacheFields(resource);
2067
2621
  resource.finalized = true;
2068
2622
  }
2623
+ function getCacheFields(resource) {
2624
+ const {
2625
+ fields
2626
+ } = resource;
2627
+ const cacheFields = new Map();
2628
+ for (const [key, value] of fields) {
2629
+ if (isNonIdentityCacheableField(value)) {
2630
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
2631
+ if (!test) {
2632
+ throw new Error(`The sourceKey '${value.sourceKey}' for the field '${key}' on ${resource.original.type} is invalid because it matches the name of an existing field`);
2633
+ }
2634
+ })(!value.sourceKey || value.sourceKey === key || !fields.has(value.sourceKey)) : {};
2635
+ const cacheKey = getFieldCacheKeyStrict(value);
2636
+ cacheFields.set(cacheKey, value);
2637
+ }
2638
+ }
2639
+ return cacheFields;
2640
+ }
2069
2641
  function walkTrait(schema, trait, fields, seen, type, debugPath) {
2070
2642
  if (seen.has(trait)) {
2071
2643
  // if the trait is in the current path, we throw a cycle error in dev.