@warp-drive/core 5.7.0-alpha.1 → 5.7.0-alpha.10

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 (41) 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/request/-private/fetch.d.ts +2 -0
  24. package/declarations/store/-types/q/schema-service.d.ts +27 -32
  25. package/declarations/store/-types/q/store.d.ts +6 -7
  26. package/declarations/store/deprecated/-private.d.ts +7 -7
  27. package/declarations/store/deprecated/store.d.ts +5 -5
  28. package/declarations/types/cache.d.ts +0 -2
  29. package/declarations/types/params.d.ts +2 -3
  30. package/declarations/types/request.d.ts +6 -6
  31. package/declarations/types/schema/fields.d.ts +348 -11
  32. package/dist/graph/-private.js +1 -1
  33. package/dist/{handler-D2jjnIA-.js → handler-SdXlte1w.js} +1 -1
  34. package/dist/index.js +2 -2
  35. package/dist/reactive.js +1157 -584
  36. package/dist/{request-state-CejVJgdj.js → request-state-CeN66aML.js} +12 -10
  37. package/dist/store/-private.js +2 -2
  38. package/dist/types/-private.js +1 -1
  39. package/dist/types/schema/fields.js +17 -2
  40. package/package.json +3 -3
  41. 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,273 @@ 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
+ // FIXME the case of field.type to string here is likely incorrect
1229
+ const schemaFields = store.schema.fields({
1230
+ type: context.field.type
1231
+ });
1232
+ for (const key of Object.keys(newValue)) {
1233
+ if (!schemaFields.has(key)) {
1234
+ throw new Error(`Field ${key} does not exist on schema object ${context.field.type}`);
1235
+ }
1236
+ }
1237
+ } else {
1238
+ ManagedObjectMap.delete(context.record);
1239
+ }
1240
+ store.cache.setAttr(context.resourceKey, context.path, newValue);
1241
+ // const peeked = peekManagedObject(self, field);
1242
+ // if (peeked) {
1243
+ // const objSignal = peeked[OBJECT_SIGNAL];
1244
+ // objSignal.isStale = true;
1245
+ // }
1246
+ return true;
715
1247
  }
1248
+ const DefaultMode = {
1249
+ '@hash': {
1250
+ get: getHashField,
1251
+ set: setHashField,
1252
+ mutable: false,
1253
+ enumerable: false,
1254
+ serializable: false
1255
+ },
1256
+ '@id': {
1257
+ get: getIdentityField,
1258
+ set: setIdentityField,
1259
+ mutable: true,
1260
+ enumerable: true,
1261
+ serializable: true
1262
+ },
1263
+ '@local': {
1264
+ get: getLocalField,
1265
+ set: setLocalField,
1266
+ mutable: true,
1267
+ enumerable: false,
1268
+ serializable: false
1269
+ },
1270
+ alias: {
1271
+ get: getAliasField,
1272
+ set: setAliasField,
1273
+ mutable: true,
1274
+ enumerable: true,
1275
+ serializable: false
1276
+ },
1277
+ array: {
1278
+ get: getArrayField,
1279
+ set: setArrayField,
1280
+ mutable: true,
1281
+ enumerable: true,
1282
+ serializable: true
1283
+ },
1284
+ attribute: {
1285
+ get: getAttributeField,
1286
+ set: setAttributeField,
1287
+ mutable: true,
1288
+ enumerable: true,
1289
+ serializable: true
1290
+ },
1291
+ belongsTo: {
1292
+ get: getBelongsToField,
1293
+ set: setBelongsToField,
1294
+ mutable: true,
1295
+ enumerable: true,
1296
+ serializable: true
1297
+ },
1298
+ collection: {
1299
+ get: getCollectionField,
1300
+ set: setCollectionField,
1301
+ mutable: true,
1302
+ enumerable: true,
1303
+ serializable: true
1304
+ },
1305
+ derived: {
1306
+ get: getDerivedField,
1307
+ set: setDerivedField,
1308
+ mutable: true,
1309
+ enumerable: true,
1310
+ serializable: false
1311
+ },
1312
+ field: {
1313
+ get: getGenericField,
1314
+ set: setGenericField,
1315
+ mutable: true,
1316
+ enumerable: true,
1317
+ serializable: true
1318
+ },
1319
+ hasMany: {
1320
+ get: getHasManyField,
1321
+ set: setHasManyField,
1322
+ mutable: true,
1323
+ enumerable: true,
1324
+ serializable: true
1325
+ },
1326
+ object: {
1327
+ get: getObjectField,
1328
+ set: setObjectField,
1329
+ mutable: true,
1330
+ enumerable: true,
1331
+ serializable: true
1332
+ },
1333
+ resource: {
1334
+ get: getResourceField,
1335
+ set: setResourceField,
1336
+ mutable: true,
1337
+ enumerable: true,
1338
+ serializable: true
1339
+ },
1340
+ 'schema-array': {
1341
+ get: getArrayField,
1342
+ set: setSchemaArrayField,
1343
+ mutable: true,
1344
+ enumerable: true,
1345
+ serializable: true
1346
+ },
1347
+ 'schema-object': {
1348
+ get: getSchemaObjectField,
1349
+ set: setSchemaObjectField,
1350
+ mutable: true,
1351
+ enumerable: true,
1352
+ serializable: true
1353
+ }
1354
+ };
716
1355
  const IgnoredGlobalFields = new Set(['length', 'nodeType', 'then', 'setInterval', 'document', STRUCTURED]);
717
1356
  const symbolList = [Destroy, RecordStore, Identifier, Editable, Parent, Checkout, Legacy, EmbeddedPath, EmbeddedField];
718
1357
  const RecordSymbols = new Set(symbolList);
@@ -737,27 +1376,41 @@ const Editables = new WeakMap();
737
1376
  */
738
1377
  // eslint-disable-next-line @typescript-eslint/no-extraneous-class
739
1378
  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;
1379
+ constructor(context) {
1380
+ const {
1381
+ store
1382
+ } = context;
1383
+ const identifier = context.resourceKey;
1384
+ const embeddedField = context.field;
1385
+ const embeddedPath = context.path;
1386
+ const isEmbedded = context.field !== null;
743
1387
  this[RecordStore] = store;
744
1388
  if (isEmbedded) {
745
1389
  this[Parent] = identifier;
746
1390
  } else {
747
1391
  this[Identifier] = identifier;
748
1392
  }
749
- const IS_EDITABLE = this[Editable] = Mode[Editable] ?? false;
750
- this[Legacy] = Mode[Legacy] ?? false;
1393
+ const IS_EDITABLE = this[Editable] = context.editable ?? false;
751
1394
  const schema = store.schema;
752
- const cache = store.cache;
753
- const identityField = schema.resource(isEmbedded ? embeddedField : identifier).identity;
1395
+ this[Legacy] = context.legacy ?? false;
1396
+ const objectType = isEmbedded ? context.value : identifier.type;
1397
+ const ResourceSchema = schema.resource(isEmbedded ? {
1398
+ type: objectType
1399
+ } : identifier);
1400
+ const identityField = ResourceSchema.identity;
754
1401
  const BoundFns = new Map();
755
1402
 
756
1403
  // prettier-ignore
757
- const extensions = !Mode[Legacy] ? null : isEmbedded ? schema.CAUTION_MEGA_DANGER_ZONE_objectExtensions(embeddedField) : schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(identifier);
1404
+ const extensions = !context.legacy ? null : isEmbedded ? schema.CAUTION_MEGA_DANGER_ZONE_objectExtensions(embeddedField, objectType) : schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(identifier);
758
1405
  this[EmbeddedField] = embeddedField;
759
1406
  this[EmbeddedPath] = embeddedPath;
760
- const fields = isEmbedded ? schema.fields(embeddedField) : schema.fields(identifier);
1407
+ const fields = isEmbedded ? schema.fields({
1408
+ type: objectType
1409
+ }) : schema.fields(identifier);
1410
+ const method = typeof schema.cacheFields === 'function' ? 'cacheFields' : 'fields';
1411
+ const cacheFields = isEmbedded ? schema[method]({
1412
+ type: objectType
1413
+ }) : schema[method](identifier);
761
1414
  const signals = withSignalStore(this);
762
1415
  const proxy = new Proxy(this, {
763
1416
  ownKeys() {
@@ -934,74 +1587,64 @@ class ReactiveResource {
934
1587
  throw new Error(`Alias fields cannot alias '@id' '@local' '@hash' or 'derived' fields`);
935
1588
  }
936
1589
  })(maybeField.kind !== 'alias' || !['@id', '@local', '@hash', 'derived'].includes(maybeField.options.kind)) : {};
1590
+ /**
1591
+ * Prop Array is the path from a resource to the field including
1592
+ * intermediate "links" on arrays,objects,schema-arrays and schema-objects.
1593
+ *
1594
+ * E.g. in the following
1595
+ *
1596
+ * ```
1597
+ * const user = {
1598
+ * addresses: [{
1599
+ * street: 'Sunset Blvd',
1600
+ * zip: 90210
1601
+ * }]
1602
+ * }
1603
+ * ```
1604
+ *
1605
+ * The propArray for "street" is ['addresses', 0, 'street']
1606
+ *
1607
+ * Prop Array follows the `cache` path to the value, not the ui path.
1608
+ * Thus, if `addresses` has a sourceKey of `user_addresses` and
1609
+ * `zip` has a sourceKey of `zip_code` then the propArray for "zip" is
1610
+ * ['user_addresses', 0, 'zip_code']
1611
+ */
937
1612
  const propArray = isEmbedded ? embeddedPath.slice() : [];
938
1613
  // we use the field.name instead of prop here because we want to use the cache-path not
939
1614
  // the record path.
940
- propArray.push(field.name);
941
- // propArray.push(prop as string);
942
-
1615
+ // SAFETY: we lie as string here because if we were to get null
1616
+ // we would be in a field kind that won't use the propArray below.
1617
+ const fieldCacheKey = getFieldCacheKey(field);
1618
+ propArray.push(fieldCacheKey);
943
1619
  switch (field.kind) {
944
1620
  case '@id':
945
- entangleSignal(signals, receiver, '@identity', null);
946
- return identifier.id;
947
1621
  case '@hash':
948
- // TODO pass actual cache value not {}
949
- return schema.hashFn(field)({}, field.options ?? null, field.name ?? null);
950
1622
  case '@local':
951
- {
952
- return computeLocal(receiver, field, prop);
953
- }
1623
+ case 'derived':
954
1624
  case 'field':
955
- entangleSignal(signals, receiver, field.name, null);
956
- return computeField(schema, cache, target, identifier, field, propArray, IS_EDITABLE);
957
1625
  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
1626
  case 'schema-array':
966
1627
  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
1628
  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]);
1629
+ case 'object':
1630
+ case 'resource':
976
1631
  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
1632
  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);
1633
+ case 'collection':
1634
+ return DefaultMode[field.kind].get({
1635
+ store,
1636
+ resourceKey: identifier,
1637
+ modeName: context.modeName,
1638
+ legacy: context.legacy,
1639
+ editable: context.editable,
1640
+ path: propArray,
1641
+ field: field,
1642
+ record: receiver,
1643
+ signals,
1644
+ value: null
1645
+ });
1003
1646
  default:
1004
- throw new Error(`Field '${String(prop)}' on '${identifier.type}' has the unknown kind '${field.kind}'`);
1647
+ assertNeverField(identifier, field, propArray);
1005
1648
  }
1006
1649
  },
1007
1650
  set(target, prop, value, receiver) {
@@ -1028,203 +1671,64 @@ class ReactiveResource {
1028
1671
  throw new Error(`Alias fields cannot alias '@id' '@local' '@hash' or 'derived' fields`);
1029
1672
  }
1030
1673
  })(maybeField.kind !== 'alias' || !['@id', '@local', '@hash', 'derived'].includes(maybeField.options.kind)) : {};
1674
+ /**
1675
+ * Prop Array is the path from a resource to the field including
1676
+ * intermediate "links" on arrays,objects,schema-arrays and schema-objects.
1677
+ *
1678
+ * E.g. in the following
1679
+ *
1680
+ * ```
1681
+ * const user = {
1682
+ * addresses: [{
1683
+ * street: 'Sunset Blvd',
1684
+ * zip: 90210
1685
+ * }]
1686
+ * }
1687
+ * ```
1688
+ *
1689
+ * The propArray for "street" is ['addresses', 0, 'street']
1690
+ *
1691
+ * Prop Array follows the `cache` path to the value, not the ui path.
1692
+ * Thus, if `addresses` has a sourceKey of `user_addresses` and
1693
+ * `zip` has a sourceKey of `zip_code` then the propArray for "zip" is
1694
+ * ['user_addresses', 0, 'zip_code']
1695
+ */
1031
1696
  const propArray = isEmbedded ? embeddedPath.slice() : [];
1032
1697
  // we use the field.name instead of prop here because we want to use the cache-path not
1033
1698
  // the record path.
1034
- propArray.push(field.name);
1035
- // propArray.push(prop as string);
1036
-
1699
+ // SAFETY: we lie as string here because if we were to get null
1700
+ // we would be in a field kind that won't use the propArray below.
1701
+ const fieldCacheKey = getFieldCacheKey(field);
1702
+ propArray.push(fieldCacheKey);
1037
1703
  switch (field.kind) {
1038
1704
  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
- }
1705
+ case '@hash':
1058
1706
  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
1707
  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
1708
  case 'attribute':
1079
- {
1080
- cache.setAttr(identifier, propArray, value);
1081
- return true;
1082
- }
1709
+ case 'derived':
1083
1710
  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
1711
  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
1712
  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
- }
1713
+ case 'object':
1714
+ case 'resource':
1205
1715
  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
1716
  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;
1717
+ case 'collection':
1718
+ return DefaultMode[field.kind].set({
1719
+ store,
1720
+ resourceKey: identifier,
1721
+ modeName: context.modeName,
1722
+ legacy: context.legacy,
1723
+ editable: context.editable,
1724
+ path: propArray,
1725
+ field: field,
1726
+ record: receiver,
1727
+ signals,
1728
+ value
1729
+ });
1226
1730
  default:
1227
- throw new Error(`Unknown field kind ${field.kind}`);
1731
+ return assertNeverField(identifier, field, propArray);
1228
1732
  }
1229
1733
  }
1230
1734
  });
@@ -1255,25 +1759,25 @@ class ReactiveResource {
1255
1759
  // TODO we should likely handle this notification here
1256
1760
  // also we should add a LOGGING flag
1257
1761
  // eslint-disable-next-line no-console
1258
- console.warn(`Notification unhandled for ${key.join(',')} on ${identifier.type}`, self);
1762
+ console.warn(`Notification unhandled for ${key.join(',')} on ${identifier.type}`, proxy);
1259
1763
  return;
1260
1764
  }
1261
1765
 
1262
1766
  // TODO we should add a LOGGING flag
1263
- // console.log(`Deep notification skipped for ${key.join('.')} on ${identifier.type}`, self);
1767
+ // console.log(`Deep notification skipped for ${key.join('.')} on ${identifier.type}`, proxy);
1264
1768
  // deep notify the key path
1265
1769
  } else {
1266
1770
  if (isEmbedded) return; // base paths never apply to embedded records
1267
1771
 
1268
1772
  // TODO determine what LOGGING flag to wrap this in if any
1269
- // console.log(`Notification for ${key} on ${identifier.type}`, self);
1773
+ // console.log(`Notification for ${key} on ${identifier.type}`, proxy);
1270
1774
  const signal = signals.get(key);
1271
1775
  if (signal) {
1272
1776
  notifyInternalSignal(signal);
1273
1777
  }
1274
- const field = fields.get(key);
1778
+ const field = cacheFields.get(key);
1275
1779
  if (field?.kind === 'array' || field?.kind === 'schema-array') {
1276
- const peeked = peekManagedArray(self, field);
1780
+ const peeked = signal?.value;
1277
1781
  if (peeked) {
1278
1782
  macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1279
1783
  if (!test) {
@@ -1285,7 +1789,7 @@ class ReactiveResource {
1285
1789
  }
1286
1790
  }
1287
1791
  if (field?.kind === 'object') {
1288
- const peeked = peekManagedObject(self, field);
1792
+ const peeked = peekManagedObject(proxy, field);
1289
1793
  if (peeked) {
1290
1794
  const objSignal = peeked[OBJECT_SIGNAL];
1291
1795
  notifyInternalSignal(objSignal);
@@ -1299,15 +1803,15 @@ class ReactiveResource {
1299
1803
  if (Array.isArray(key)) ;else {
1300
1804
  if (isEmbedded) return; // base paths never apply to embedded records
1301
1805
 
1302
- const field = fields.get(key);
1806
+ const field = cacheFields.get(key);
1303
1807
  macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1304
1808
  if (!test) {
1305
- throw new Error(`Expected relationshp ${key} to be the name of a field`);
1809
+ throw new Error(`Expected relationship ${key} to be the name of a field`);
1306
1810
  }
1307
1811
  })(field) : {};
1308
1812
  if (field.kind === 'belongsTo') {
1309
1813
  // TODO determine what LOGGING flag to wrap this in if any
1310
- // console.log(`Notification for ${key} on ${identifier.type}`, self);
1814
+ // console.log(`Notification for ${key} on ${identifier.type}`, proxy);
1311
1815
  const signal = signals.get(key);
1312
1816
  if (signal) {
1313
1817
  notifyInternalSignal(signal);
@@ -1315,9 +1819,12 @@ class ReactiveResource {
1315
1819
  // FIXME
1316
1820
  } else if (field.kind === 'resource') ;else if (field.kind === 'hasMany') {
1317
1821
  if (field.options.linksMode) {
1318
- const peeked = peekManagedArray(self, field);
1319
- if (peeked) {
1320
- notifyInternalSignal(peeked[ARRAY_SIGNAL]);
1822
+ const signal = signals.get(key);
1823
+ if (signal) {
1824
+ const peeked = signal.value;
1825
+ if (peeked) {
1826
+ notifyInternalSignal(peeked[ARRAY_SIGNAL]);
1827
+ }
1321
1828
  }
1322
1829
  return;
1323
1830
  }
@@ -1325,7 +1832,7 @@ class ReactiveResource {
1325
1832
  if (!test) {
1326
1833
  throw new Error(`Can only use hasMany fields when the resource is in legacy mode`);
1327
1834
  }
1328
- })(Mode[Legacy]) : {};
1835
+ })(context.legacy) : {};
1329
1836
  if (schema._kind('@legacy', 'hasMany').notify(store, proxy, identifier, field)) {
1330
1837
  macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1331
1838
  if (!test) {
@@ -1381,10 +1888,17 @@ function _CHECKOUT(record) {
1381
1888
  if (isEmbedded) {
1382
1889
  throw new Error(`Cannot checkout an embedded record (yet)`);
1383
1890
  }
1384
- const editableRecord = new ReactiveResource(record[RecordStore], record[Identifier], {
1385
- [Editable]: true,
1386
- [Legacy]: record[Legacy]
1387
- }, isEmbedded, embeddedType, embeddedPath);
1891
+ const legacy = record[Legacy];
1892
+ const editableRecord = new ReactiveResource({
1893
+ store: record[RecordStore],
1894
+ resourceKey: record[Identifier],
1895
+ modeName: legacy ? 'legacy' : 'polaris',
1896
+ legacy: legacy,
1897
+ editable: true,
1898
+ path: null,
1899
+ field: null,
1900
+ value: null
1901
+ });
1388
1902
  setRecordIdentifier(editableRecord, recordIdentifierFor(record));
1389
1903
  return Promise.resolve(editableRecord);
1390
1904
  }
@@ -1396,6 +1910,17 @@ function _DESTROY(record) {
1396
1910
  record.isDestroyed = true;
1397
1911
  }
1398
1912
  record[RecordStore].notifications.unsubscribe(record.___notifications);
1913
+
1914
+ // FIXME we need a way to also unsubscribe all SchemaObjects when the primary
1915
+ // resource is destroyed.
1916
+ }
1917
+ function assertNeverField(identifier, field, path) {
1918
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1919
+ {
1920
+ throw new Error(`Cannot use unknown field kind ${field.kind} on <${identifier.type}>.${Array.isArray(path) ? path.join('.') : path}`);
1921
+ }
1922
+ })() : {};
1923
+ return false;
1399
1924
  }
1400
1925
  function instantiateRecord(store, identifier, createArgs) {
1401
1926
  const schema = store.schema;
@@ -1405,11 +1930,17 @@ function instantiateRecord(store, identifier, createArgs) {
1405
1930
  throw new Error(`Expected a resource schema`);
1406
1931
  }
1407
1932
  })(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
1933
+ const legacy = resourceSchema?.legacy ?? false;
1934
+ const editable = legacy || store.cache.isNew(identifier);
1935
+ const record = new ReactiveResource({
1936
+ store,
1937
+ resourceKey: identifier,
1938
+ modeName: legacy ? 'legacy' : 'polaris',
1939
+ legacy: legacy,
1940
+ editable: editable,
1941
+ path: null,
1942
+ field: null,
1943
+ value: null
1413
1944
  });
1414
1945
  if (createArgs) {
1415
1946
  Object.assign(record, createArgs);
@@ -1546,7 +2077,7 @@ function getExt(extCache, type, extName) {
1546
2077
  function hasObjectSchema(field) {
1547
2078
  return 'kind' in field && (field.kind === 'schema-array' || field.kind === 'schema-object');
1548
2079
  }
1549
- function processExtensions(schema, field, scenario) {
2080
+ function processExtensions(schema, field, scenario, resolvedType) {
1550
2081
  // if we're looking up extensions for a resource, there is no
1551
2082
  // merging required so if we have no objectExtensions
1552
2083
  // we are done.
@@ -1573,12 +2104,16 @@ function processExtensions(schema, field, scenario) {
1573
2104
  if (!hasObjectSchema(field)) {
1574
2105
  return null;
1575
2106
  }
1576
- return schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(field);
2107
+ return schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(resolvedType ? {
2108
+ type: resolvedType
2109
+ } : field);
1577
2110
  }
1578
2111
 
1579
2112
  // if we have made it here, we have extensions, lets check if there's
1580
2113
  // 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;
2114
+ const baseExtensions = scenario === 'resource' && hasObjectSchema(field) ? schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(field) : scenario === 'object' && hasObjectSchema(field) ? schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(resolvedType ? {
2115
+ type: resolvedType
2116
+ } : field) : null;
1582
2117
  if (!baseExtensions && extensions.length === 1) {
1583
2118
  const value = getExt(extCache, type, extensions[0]);
1584
2119
  fieldCache[type].set(field, value);
@@ -1836,16 +2371,21 @@ class SchemaService {
1836
2371
  relationships[field.name] = field;
1837
2372
  }
1838
2373
  }
2374
+ const cacheFields = null;
1839
2375
  const traits = new Set(isResourceSchema(schema) ? schema.traits : []);
1840
2376
  const finalized = traits.size === 0;
1841
2377
  const internalSchema = {
1842
2378
  original: schema,
1843
2379
  finalized,
1844
2380
  fields,
2381
+ cacheFields,
1845
2382
  relationships,
1846
2383
  attributes,
1847
2384
  traits
1848
2385
  };
2386
+ if (traits.size === 0) {
2387
+ internalSchema.cacheFields = getCacheFields(internalSchema);
2388
+ }
1849
2389
  this._schemas.set(schema.type, internalSchema);
1850
2390
  }
1851
2391
 
@@ -1902,13 +2442,13 @@ class SchemaService {
1902
2442
  }
1903
2443
  CAUTION_MEGA_DANGER_ZONE_resourceExtensions(resource) {
1904
2444
  const schema = this.resource(resource);
1905
- return processExtensions(this, schema, 'resource');
2445
+ return processExtensions(this, schema, 'resource', null);
1906
2446
  }
1907
- CAUTION_MEGA_DANGER_ZONE_objectExtensions(field) {
1908
- return processExtensions(this, field, 'object');
2447
+ CAUTION_MEGA_DANGER_ZONE_objectExtensions(field, resolvedType) {
2448
+ return processExtensions(this, field, 'object', resolvedType);
1909
2449
  }
1910
2450
  CAUTION_MEGA_DANGER_ZONE_arrayExtensions(field) {
1911
- return processExtensions(this, field, 'array');
2451
+ return processExtensions(this, field, 'array', null);
1912
2452
  }
1913
2453
  CAUTION_MEGA_DANGER_ZONE_hasExtension(ext) {
1914
2454
  return this._extensions[ext.kind].has(ext.name);
@@ -1976,6 +2516,20 @@ class SchemaService {
1976
2516
  }
1977
2517
  return schema.fields;
1978
2518
  }
2519
+ cacheFields({
2520
+ type
2521
+ }) {
2522
+ const schema = this._schemas.get(type);
2523
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
2524
+ if (!test) {
2525
+ throw new Error(`No schema defined for ${type}`);
2526
+ }
2527
+ })(schema) : {};
2528
+ if (!schema.finalized) {
2529
+ finalizeResource(this, schema);
2530
+ }
2531
+ return schema.cacheFields;
2532
+ }
1979
2533
  hasResource(resource) {
1980
2534
  return this._schemas.has(resource.type);
1981
2535
  }
@@ -2064,8 +2618,27 @@ function finalizeResource(schema, resource) {
2064
2618
  }
2065
2619
  mergeMap(fields, resource.fields);
2066
2620
  resource.fields = fields;
2621
+ resource.cacheFields = getCacheFields(resource);
2067
2622
  resource.finalized = true;
2068
2623
  }
2624
+ function getCacheFields(resource) {
2625
+ const {
2626
+ fields
2627
+ } = resource;
2628
+ const cacheFields = new Map();
2629
+ for (const [key, value] of fields) {
2630
+ if (isNonIdentityCacheableField(value)) {
2631
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
2632
+ if (!test) {
2633
+ 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`);
2634
+ }
2635
+ })(!value.sourceKey || value.sourceKey === key || !fields.has(value.sourceKey)) : {};
2636
+ const cacheKey = getFieldCacheKeyStrict(value);
2637
+ cacheFields.set(cacheKey, value);
2638
+ }
2639
+ }
2640
+ return cacheFields;
2641
+ }
2069
2642
  function walkTrait(schema, trait, fields, seen, type, debugPath) {
2070
2643
  if (seen.has(trait)) {
2071
2644
  // if the trait is in the current path, we throw a cycle error in dev.