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

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 (46) 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 +5 -8
  4. package/declarations/reactive/-private/fields/managed-object.d.ts +7 -9
  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 +6 -21
  21. package/declarations/reactive/-private/schema.d.ts +6 -2
  22. package/declarations/reactive/-private/symbols.d.ts +1 -6
  23. package/declarations/reactive/-private.d.ts +1 -1
  24. package/declarations/reactive.d.ts +1 -0
  25. package/declarations/request/-private/fetch.d.ts +2 -0
  26. package/declarations/store/-types/q/schema-service.d.ts +27 -32
  27. package/declarations/store/-types/q/store.d.ts +6 -7
  28. package/declarations/store/deprecated/-private.d.ts +7 -7
  29. package/declarations/store/deprecated/store.d.ts +5 -5
  30. package/declarations/types/-private.d.ts +1 -1
  31. package/declarations/types/cache.d.ts +0 -2
  32. package/declarations/types/params.d.ts +2 -3
  33. package/declarations/types/request.d.ts +6 -6
  34. package/declarations/types/schema/fields.d.ts +377 -13
  35. package/dist/graph/-private.js +1 -1
  36. package/dist/{handler-D2jjnIA-.js → handler-SdXlte1w.js} +1 -1
  37. package/dist/index.js +2 -2
  38. package/dist/reactive/-private.js +1 -1
  39. package/dist/reactive.js +1171 -603
  40. package/dist/{request-state-CejVJgdj.js → request-state-CeN66aML.js} +12 -10
  41. package/dist/store/-private.js +2 -2
  42. package/dist/{symbols-SIstXMLI.js → symbols-BoONANuz.js} +2 -7
  43. package/dist/types/-private.js +1 -1
  44. package/dist/types/schema/fields.js +21 -2
  45. package/package.json +3 -3
  46. 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, C as Context, D as Destroy, a as Checkout } from "./symbols-BoONANuz.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 = context.editable ?? false;
78
+ this[Context] = context;
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,455 @@ const desc = {
302
390
  };
303
391
  // compat(desc);
304
392
  Object.defineProperty(ManagedArray.prototype, '[]', desc);
305
- const ObjectSymbols = new Set([OBJECT_SIGNAL, Parent, SOURCE, Editable, EmbeddedPath]);
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
+ field
412
+ } = context;
413
+ const {
414
+ cache
415
+ } = store;
416
+ let rawValue = context.editable ? cache.getAttr(resourceKey, path) : cache.getRemoteAttr(resourceKey, path);
417
+ if (!rawValue && field.kind === 'schema-array' && field.options?.defaultValue) {
418
+ rawValue = [];
419
+ }
420
+ if (!rawValue) {
421
+ return null;
422
+ }
423
+ managedArray = new ManagedArray(context, record, rawValue);
424
+ signal.value = managedArray;
425
+ }
426
+ return managedArray;
427
+ }
428
+ function setArrayField(context) {
429
+ const {
430
+ field,
431
+ record,
432
+ value
433
+ } = context;
434
+ const {
435
+ cache,
436
+ schema
437
+ } = context.store;
438
+ const fieldSignal = peekInternalSignal(context.signals, context.path.at(-1));
439
+ const peeked = fieldSignal?.value;
440
+ const transform = field.type ? schema.transformation(field) : null;
441
+ const rawValue = field.type ? value.map(item => transform.serialize(item, field.options ?? null, record)) : value?.slice();
442
+ cache.setAttr(context.resourceKey, context.path, rawValue);
443
+ if (peeked) {
444
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
445
+ if (!test) {
446
+ throw new Error(`Expected the peekManagedArray for ${field.kind} to return a ManagedArray`);
447
+ }
448
+ })(ARRAY_SIGNAL in peeked) : {};
449
+ const arrSignal = peeked[ARRAY_SIGNAL];
450
+ arrSignal.isStale = true;
451
+ // TODO run array destroy?
452
+ }
453
+ if (!Array.isArray(rawValue) && fieldSignal) {
454
+ fieldSignal.value = null;
455
+ }
456
+ return true;
457
+ }
458
+ function getAttributeField(context) {
459
+ entangleSignal(context.signals, context.record, context.path.at(-1), null);
460
+ const {
461
+ cache
462
+ } = context.store;
463
+ return context.editable ? cache.getAttr(context.resourceKey, context.path) : cache.getRemoteAttr(context.resourceKey, context.path);
464
+ }
465
+ function setAttributeField(context) {
466
+ context.store.cache.setAttr(context.resourceKey, context.path, context.value);
467
+ return true;
468
+ }
469
+ const InvalidKinds = ['alias', 'derived', '@local'];
470
+ function isInvalidKind(kind) {
471
+ return InvalidKinds.includes(kind);
472
+ }
473
+ function isNonIdentityCacheableField(field) {
474
+ return !isInvalidKind(field.kind) && field.kind !== '@id' && field.kind !== '@hash';
475
+ }
476
+ function getFieldCacheKeyStrict(field) {
477
+ return field.sourceKey || field.name;
478
+ }
479
+ function getFieldCacheKey(field) {
480
+ return 'sourceKey' in field && field.sourceKey ? field.sourceKey : field.name;
481
+ }
482
+ function getBelongsToField(context) {
483
+ entangleSignal(context.signals, context.record, context.path.at(-1), null);
484
+ const {
485
+ field,
486
+ resourceKey,
487
+ store
488
+ } = context;
489
+ const {
490
+ schema,
491
+ cache
492
+ } = store;
493
+ if (field.options.linksMode) {
494
+ const rawValue = context.editable ? cache.getRelationship(resourceKey, getFieldCacheKeyStrict(field)) : cache.getRemoteRelationship(resourceKey, getFieldCacheKeyStrict(field));
495
+ return rawValue.data ? store.peekRecord(rawValue.data) : null;
496
+ }
497
+
498
+ // FIXME move this to a "LegacyMode" make this part of "PolarisMode"
499
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
500
+ if (!test) {
501
+ throw new Error(`Can only use belongsTo fields when the resource is in legacy mode`);
502
+ }
503
+ })(context.legacy) : {};
504
+ return schema._kind('@legacy', 'belongsTo').get(store, context.record, resourceKey, field);
505
+ }
506
+ function setBelongsToField(context) {
507
+ const {
508
+ store
509
+ } = context;
510
+ const {
511
+ schema
512
+ } = store;
513
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
514
+ if (!test) {
515
+ throw new Error(`Can only mutate belongsTo fields when the resource is in legacy mode`);
516
+ }
517
+ })(context.legacy) : {};
518
+ schema._kind('@legacy', 'belongsTo').set(store, context.record, context.resourceKey, context.field, context.value);
519
+ return true;
520
+ }
521
+ function getCollectionField(context) {
522
+ entangleSignal(context.signals, context.record, context.path.at(-1), null);
523
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
524
+ {
525
+ throw new Error(`Accessing collection fields is not yet implemented`);
526
+ }
527
+ })() : {};
528
+ }
529
+ function setCollectionField(context) {
530
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
531
+ {
532
+ throw new Error(`Setting collection fields is not yet implemented`);
533
+ }
534
+ })() : {};
535
+ return false;
536
+ }
537
+ function getDerivedField(context) {
538
+ const {
539
+ schema
540
+ } = context.store;
541
+ return schema.derivation(context.field)(context.record, context.field.options ?? null, context.field.name);
542
+ }
543
+ function setDerivedField(context) {
544
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
545
+ {
546
+ throw new Error(`ILLEGAL SET: Cannot set '${context.path.join('.')}' on '${context.resourceKey.type}' as ${context.field.kind} fields are not mutable`);
547
+ }
548
+ })() : {};
549
+ return false;
550
+ }
551
+ function getGenericField(context) {
552
+ entangleSignal(context.signals, context.record, context.path.at(-1), null);
553
+ const {
554
+ cache,
555
+ schema
556
+ } = context.store;
557
+ const rawValue = context.editable ? cache.getAttr(context.resourceKey, context.path) : cache.getRemoteAttr(context.resourceKey, context.path);
558
+ const {
559
+ field
560
+ } = context;
561
+ if (!field.type) {
562
+ return rawValue;
563
+ }
564
+ const transform = schema.transformation(field);
565
+ return transform.hydrate(rawValue, field.options ?? null, context.record);
566
+ }
567
+ function setGenericField(context) {
568
+ const {
569
+ cache,
570
+ schema
571
+ } = context.store;
572
+ const {
573
+ field
574
+ } = context;
575
+ if (!field.type) {
576
+ cache.setAttr(context.resourceKey, context.path, context.value);
577
+ return true;
578
+ }
579
+ const transform = schema.transformation(field);
580
+ const rawValue = transform.serialize(context.value, field.options ?? null, context.record);
581
+ cache.setAttr(context.resourceKey, context.path, rawValue);
582
+ return true;
583
+ }
584
+ class ManyArrayManager {
585
+ constructor(record, editable) {
586
+ const context = record[Context];
587
+ this.record = record;
588
+ this.store = context.store;
589
+ this.identifier = context.resourceKey;
590
+ this.editable = editable;
591
+ }
592
+ _syncArray(array) {
593
+ const method = this.editable ? 'getRelationship' : 'getRemoteRelationship';
594
+ // FIXME field needs to use sourceKey
595
+ const rawValue = this.store.cache[method](this.identifier, array.key);
596
+ if (rawValue.meta) {
597
+ array.meta = rawValue.meta;
598
+ }
599
+ if (rawValue.links) {
600
+ array.links = rawValue.links;
601
+ }
602
+ const currentState = array[SOURCE$1];
603
+
604
+ // unlike in the normal RecordArray case, we don't need to divorce the reference
605
+ // because we don't need to worry about associate/disassociate since the graph
606
+ // takes care of that for us
607
+ if (currentState !== rawValue.data) {
608
+ currentState.length = 0;
609
+ fastPush(currentState, rawValue.data);
610
+ }
611
+ }
612
+ reloadHasMany(key, options) {
613
+ // FIXME field needs to use sourceKey
614
+ const field = this.store.schema.fields(this.identifier).get(key);
615
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
616
+ if (!test) {
617
+ throw new Error(`Expected a hasMany field for ${key}`);
618
+ }
619
+ })(field?.kind === 'hasMany') : {};
620
+ const cacheOptions = options ? extractCacheOptions(options) : {
621
+ reload: true
622
+ };
623
+ cacheOptions.types = [field.type];
624
+ const rawValue = this.store.cache.getRelationship(this.identifier, key);
625
+ const req = {
626
+ url: getRelatedLink(rawValue),
627
+ op: 'findHasMany',
628
+ method: 'GET',
629
+ records: rawValue.data,
630
+ cacheOptions,
631
+ options: {
632
+ field,
633
+ identifier: this.identifier,
634
+ links: rawValue.links,
635
+ meta: rawValue.meta
636
+ },
637
+ [EnableHydration]: false
638
+ };
639
+ return this.store.request(req);
640
+ }
641
+ mutate(mutation) {
642
+ this.store.cache.mutate(mutation);
643
+ }
644
+ }
645
+ function getRelatedLink(resource) {
646
+ const related = resource.links?.related;
647
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
648
+ if (!test) {
649
+ throw new Error(`Expected a related link`);
650
+ }
651
+ })(related) : {};
652
+ return typeof related === 'object' ? related.href : related;
653
+ }
654
+ function extractCacheOptions(options) {
655
+ const cacheOptions = {};
656
+ if ('reload' in options) {
657
+ cacheOptions.reload = options.reload;
658
+ }
659
+ if ('backgroundReload' in options) {
660
+ cacheOptions.backgroundReload = options.backgroundReload;
661
+ }
662
+ return cacheOptions;
663
+ }
664
+ function getHasManyField(context) {
665
+ const signal = entangleSignal(context.signals, context.record, context.path.at(-1), null);
666
+ const {
667
+ store,
668
+ field
669
+ } = context;
670
+ if (field.options.linksMode) {
671
+ const {
672
+ record
673
+ } = context;
674
+ // the thing we hand out needs to know its owner and path in a private manner
675
+ // its "address" is the parent identifier (identifier) + field name (field.name)
676
+ // in the nested object case field name here is the full dot path from root resource to this value
677
+ // its "key" is the field on the parent record
678
+ // its "owner" is the parent record
679
+
680
+ const cached = signal.value;
681
+ if (cached) {
682
+ return cached;
683
+ }
684
+ const {
685
+ editable,
686
+ resourceKey
687
+ } = context;
688
+ const {
689
+ cache
690
+ } = store;
691
+ const rawValue = cache.getRelationship(resourceKey, getFieldCacheKeyStrict(field));
692
+ if (!rawValue) {
693
+ return null;
694
+ }
695
+ const managedArray = new RelatedCollection({
696
+ store,
697
+ type: field.type,
698
+ identifier: resourceKey,
699
+ cache,
700
+ field: context.legacy ? field : undefined,
701
+ // we divorce the reference here because ManyArray mutates the target directly
702
+ // before sending the mutation op to the cache. We may be able to avoid this in the future
703
+ identifiers: rawValue.data?.slice(),
704
+ key: field.name,
705
+ meta: rawValue.meta || null,
706
+ links: rawValue.links || null,
707
+ isPolymorphic: field.options.polymorphic ?? false,
708
+ isAsync: field.options.async ?? false,
709
+ // TODO: Grab the proper value
710
+ _inverseIsAsync: false,
711
+ // @ts-expect-error Typescript doesn't have a way for us to thread the generic backwards so it infers unknown instead of T
712
+ manager: new ManyArrayManager(record, editable),
713
+ isLoaded: true,
714
+ allowMutation: editable
715
+ });
716
+ signal.value = managedArray;
717
+ return managedArray;
718
+ }
719
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
720
+ if (!test) {
721
+ throw new Error(`Can only use hasMany fields when the resource is in legacy mode`);
722
+ }
723
+ })(context.legacy) : {};
724
+ return store.schema._kind('@legacy', 'hasMany').get(store, context.record, context.resourceKey, field);
725
+ }
726
+ function setHasManyField(context) {
727
+ const {
728
+ store
729
+ } = context;
730
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
731
+ if (!test) {
732
+ throw new Error(`Can only use hasMany fields when the resource is in legacy mode`);
733
+ }
734
+ })(context.legacy) : {};
735
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
736
+ if (!test) {
737
+ throw new Error(`You must pass an array of records to set a hasMany relationship`);
738
+ }
739
+ })(Array.isArray(context.value)) : {};
740
+ store.schema._kind('@legacy', 'hasMany').set(store, context.record, context.resourceKey, context.field, context.value);
741
+ return true;
742
+ }
743
+ function getHashField(context) {
744
+ const {
745
+ field,
746
+ path,
747
+ resourceKey
748
+ } = context;
749
+ const {
750
+ schema,
751
+ cache
752
+ } = context.store;
753
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
754
+ if (!test) {
755
+ throw new Error(`Cannot use a ${field.kind} directly on a resource.`);
756
+ }
757
+ })(Array.isArray(path) && path.length > 1) : {};
758
+ const realPath = path.slice(0, -1);
759
+ const rawData = context.editable ? cache.getAttr(resourceKey, realPath) : cache.getRemoteAttr(resourceKey, realPath);
760
+ return schema.hashFn(field)(rawData, field.options ?? null, field.name ?? null);
761
+ }
762
+ function setHashField(context) {
763
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
764
+ {
765
+ 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`);
766
+ }
767
+ })() : {};
768
+ return false;
769
+ }
770
+ function getIdentityField(context) {
771
+ entangleSignal(context.signals, context.record, '@identity', null);
772
+ return context.resourceKey.id;
773
+ }
774
+ function setIdentityField(context) {
775
+ const {
776
+ value,
777
+ resourceKey,
778
+ store
779
+ } = context;
780
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
781
+ if (!test) {
782
+ throw new Error(`Expected to receive a string id`);
783
+ }
784
+ })(typeof value === 'string' && value.length) : {};
785
+ const normalizedId = String(value);
786
+ const didChange = normalizedId !== resourceKey.id;
787
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
788
+ if (!test) {
789
+ throw new Error(`Cannot set ${resourceKey.type} record's id to ${normalizedId}, because id is already ${resourceKey.id}`);
790
+ }
791
+ })(!didChange || resourceKey.id === null) : {};
792
+ if (normalizedId !== null && didChange) {
793
+ store._instanceCache.setRecordId(resourceKey, normalizedId);
794
+ store.notifications.notify(resourceKey, 'identity');
795
+ }
796
+ return true;
797
+ }
798
+ function getLocalField(context) {
799
+ const {
800
+ field
801
+ } = context;
802
+ const signal = getOrCreateInternalSignal(context.signals, context.record, field.name, field.options?.defaultValue ?? null);
803
+ consumeInternalSignal(signal);
804
+ return signal.value;
805
+ }
806
+ function setLocalField(context) {
807
+ const {
808
+ value
809
+ } = context;
810
+ const signal = getOrCreateInternalSignal(context.signals, context.record, context.field.name, value);
811
+ if (signal.value !== value) {
812
+ signal.value = value;
813
+ notifyInternalSignal(signal);
814
+ }
815
+ return true;
816
+ }
817
+ const ObjectSymbols = new Set([OBJECT_SIGNAL, Context, SOURCE]);
306
818
 
307
819
  // const ignoredGlobalFields = new Set<string>(['setInterval', 'nodeType', 'nodeName', 'length', 'document', STRUCTURED]);
308
820
 
309
821
  // eslint-disable-next-line @typescript-eslint/no-extraneous-class
310
822
  class ManagedObject {
311
- constructor(schema, cache, field, data, identifier, path, owner, editable, legacy) {
823
+ constructor(context) {
824
+ const {
825
+ field,
826
+ path
827
+ } = context;
312
828
  // eslint-disable-next-line @typescript-eslint/no-this-alias
313
829
  const self = this;
314
- this[SOURCE] = {
315
- ...data
316
- };
830
+ this[SOURCE] = Object.assign({}, context.value);
317
831
  const signals = withSignalStore(this);
318
832
  const _SIGNAL = this[OBJECT_SIGNAL] = entangleSignal(signals, this, OBJECT_SIGNAL, undefined);
319
- this[Editable] = editable;
320
- this[Legacy] = legacy;
321
- this[Parent] = identifier;
322
- this[EmbeddedPath] = path;
833
+ this[Context] = context;
834
+ const identifier = context.resourceKey;
835
+ const {
836
+ cache,
837
+ schema
838
+ } = context.store;
323
839
 
324
840
  // prettier-ignore
325
- const extensions = !legacy ? null : schema.CAUTION_MEGA_DANGER_ZONE_objectExtensions(field);
841
+ const extensions = !context.legacy ? null : schema.CAUTION_MEGA_DANGER_ZONE_objectExtensions(field, null);
326
842
  const proxy = new Proxy(this[SOURCE], {
327
843
  ownKeys() {
328
844
  return Object.keys(self[SOURCE]);
@@ -332,7 +848,7 @@ class ManagedObject {
332
848
  },
333
849
  getOwnPropertyDescriptor(target, prop) {
334
850
  return {
335
- writable: editable,
851
+ writable: context.editable,
336
852
  enumerable: true,
337
853
  configurable: true
338
854
  };
@@ -366,11 +882,9 @@ class ManagedObject {
366
882
  if (newData && newData !== self[SOURCE]) {
367
883
  if (field.type) {
368
884
  const transform = schema.transformation(field);
369
- newData = transform.hydrate(newData, field.options ?? null, owner);
885
+ newData = transform.hydrate(newData, field.options ?? null, context.record);
370
886
  }
371
- self[SOURCE] = {
372
- ...newData
373
- }; // Add type assertion for newData
887
+ self[SOURCE] = Object.assign({}, newData); // Add type assertion for newData
374
888
  }
375
889
  }
376
890
 
@@ -396,7 +910,7 @@ class ManagedObject {
396
910
  if (!test) {
397
911
  throw new Error(`Cannot set read-only property '${String(prop)}' on ManagedObject`);
398
912
  }
399
- })(editable) : {};
913
+ })(context.editable) : {};
400
914
 
401
915
  // since objects function as dictionaries, we can't defer to schema/data before extensions
402
916
  // unless the prop is in the existing data.
@@ -411,151 +925,29 @@ class ManagedObject {
411
925
  cache.setAttr(identifier, path, self[SOURCE]);
412
926
  } else {
413
927
  const transform = schema.transformation(field);
414
- const val = transform.serialize(self[SOURCE], field.options ?? null, owner);
928
+ const val = transform.serialize(self[SOURCE], field.options ?? null, context.record);
415
929
  cache.setAttr(identifier, path, val);
416
930
  }
417
931
  _SIGNAL.isStale = true;
418
932
  return true;
419
933
  }
420
934
  });
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;
935
+ return proxy;
498
936
  }
499
- return cacheOptions;
500
937
  }
501
- const ManagedArrayMap = getOrSetGlobal('ManagedArrayMap', new Map());
502
938
  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
939
  function peekManagedObject(record, field) {
516
940
  const managedObjectMapForRecord = ManagedObjectMap.get(record);
517
941
  if (managedObjectMapForRecord) {
518
942
  return managedObjectMapForRecord.get(field.name);
519
943
  }
520
944
  }
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) {
945
+ function getObjectField(context) {
946
+ entangleSignal(context.signals, context.record, context.path.at(-1), null);
947
+ const {
948
+ record,
949
+ field
950
+ } = context;
559
951
  const managedObjectMapForRecord = ManagedObjectMap.get(record);
560
952
  let managedObject;
561
953
  if (managedObjectMapForRecord) {
@@ -564,7 +956,16 @@ function computeObject(schema, cache, record, identifier, field, path, editable,
564
956
  if (managedObject) {
565
957
  return managedObject;
566
958
  } else {
567
- let rawValue = editable ? cache.getAttr(identifier, path) : cache.getRemoteAttr(identifier, path);
959
+ const {
960
+ store,
961
+ resourceKey,
962
+ path
963
+ } = context;
964
+ const {
965
+ cache,
966
+ schema
967
+ } = store;
968
+ let rawValue = context.editable ? cache.getAttr(resourceKey, path) : cache.getRemoteAttr(resourceKey, path);
568
969
  if (!rawValue) {
569
970
  return null;
570
971
  }
@@ -572,7 +973,18 @@ function computeObject(schema, cache, record, identifier, field, path, editable,
572
973
  const transform = schema.transformation(field);
573
974
  rawValue = transform.hydrate(rawValue, field.options ?? null, record);
574
975
  }
575
- managedObject = new ManagedObject(schema, cache, field, rawValue, identifier, path, record, editable, legacy);
976
+ managedObject = new ManagedObject({
977
+ store,
978
+ resourceKey,
979
+ modeName: context.modeName,
980
+ legacy: context.legacy,
981
+ editable: context.editable,
982
+ path,
983
+ field,
984
+ record,
985
+ signals: context.signals,
986
+ value: rawValue
987
+ });
576
988
  if (!managedObjectMapForRecord) {
577
989
  ManagedObjectMap.set(record, new Map([[field.name, managedObject]]));
578
990
  } else {
@@ -581,48 +993,64 @@ function computeObject(schema, cache, record, identifier, field, path, editable,
581
993
  }
582
994
  return managedObject;
583
995
  }
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;
996
+ function setObjectField(context) {
997
+ const {
998
+ field,
999
+ value,
1000
+ record
1001
+ } = context;
1002
+ const {
1003
+ cache,
1004
+ schema
1005
+ } = context.store;
1006
+ if (!field.type) {
1007
+ let newValue = value;
1008
+ if (value !== null) {
1009
+ newValue = {
1010
+ ...value
1011
+ };
1012
+ } else {
1013
+ ManagedObjectMap.delete(record);
596
1014
  }
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);
1015
+ cache.setAttr(context.resourceKey, context.path, newValue);
1016
+ const peeked = peekManagedObject(record, field);
1017
+ if (peeked) {
1018
+ const objSignal = peeked[OBJECT_SIGNAL];
1019
+ objSignal.isStale = true;
1020
+ }
1021
+ return true;
607
1022
  }
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);
1023
+ const transform = schema.transformation(field);
1024
+ const rawValue = transform.serialize({
1025
+ ...value
1026
+ }, field.options ?? null, record);
1027
+ cache.setAttr(context.resourceKey, context.path, rawValue);
1028
+ const peeked = peekManagedObject(record, field);
1029
+ if (peeked) {
1030
+ const objSignal = peeked[OBJECT_SIGNAL];
1031
+ objSignal.isStale = true;
1032
+ }
1033
+ return true;
615
1034
  }
1035
+
616
1036
  // TODO probably this should just be a Document
617
1037
  // but its separate until we work out the lid situation
618
1038
  class ResourceRelationship {
619
- constructor(store, cache, parent, identifier, field, name, editable) {
620
- const rawValue = editable ? cache.getRelationship(identifier, name) : cache.getRemoteRelationship(identifier, name);
1039
+ constructor(context) {
1040
+ const {
1041
+ store,
1042
+ resourceKey
1043
+ } = context;
1044
+ const {
1045
+ cache
1046
+ } = store;
1047
+ const name = getFieldCacheKeyStrict(context.field);
1048
+ const rawValue = context.editable ? cache.getRelationship(resourceKey, name) : cache.getRemoteRelationship(resourceKey, name);
621
1049
 
622
1050
  // TODO setup true lids for relationship documents
623
1051
  // @ts-expect-error we need to give relationship documents a lid
624
1052
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
625
- this.lid = rawValue.lid ?? rawValue.links?.self ?? `relationship:${identifier.lid}.${name}`;
1053
+ this.lid = rawValue.lid ?? rawValue.links?.self ?? `relationship:${resourceKey.lid}.${name}`;
626
1054
  this.data = rawValue.data ? store.peekRecord(rawValue.data) : null;
627
1055
  this.name = name;
628
1056
  if (macroCondition(getGlobalConfig().WarpDrive.env.DEBUG)) {
@@ -632,19 +1060,18 @@ class ResourceRelationship {
632
1060
  this.links = rawValue.links ?? {};
633
1061
  this.meta = rawValue.meta ?? {};
634
1062
  }
635
- this[RecordStore] = store;
636
- this[Parent] = parent;
1063
+ this[Context] = context;
637
1064
  }
638
1065
  fetch(options) {
639
1066
  const url = options?.url ?? getHref(this.links.related) ?? getHref(this.links.self) ?? null;
640
1067
  if (!url) {
641
- throw new Error(`Cannot ${options?.method ?? 'fetch'} ${this[Parent][Identifier].type}.${String(this.name)} because it has no related link`);
1068
+ throw new Error(`Cannot ${options?.method ?? 'fetch'} ${this[Context].resourceKey.type}.${String(this.name)} because it has no related link`);
642
1069
  }
643
1070
  const request = Object.assign({
644
1071
  url,
645
1072
  method: 'GET'
646
1073
  }, options);
647
- return this[RecordStore].request(request);
1074
+ return this[Context].store.request(request);
648
1075
  }
649
1076
  }
650
1077
  defineSignal(ResourceRelationship.prototype, 'data', null);
@@ -659,62 +1086,276 @@ function getHref(link) {
659
1086
  }
660
1087
  return link.href;
661
1088
  }
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`);
1089
+ function getResourceField(context) {
1090
+ entangleSignal(context.signals, context.record, context.path.at(-1), null);
1091
+ return new ResourceRelationship(context);
1092
+ }
1093
+ function setResourceField(context) {
1094
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1095
+ {
1096
+ throw new Error(`setting resource relationships is not yet supported`);
1097
+ }
1098
+ })() : {};
1099
+ return false;
1100
+ }
1101
+ function setSchemaArrayField(context) {
1102
+ const arrayValue = context.value?.slice();
1103
+ const fieldSignal = peekInternalSignal(context.signals, context.path.at(-1));
1104
+ const peeked = fieldSignal?.value;
1105
+ context.store.cache.setAttr(context.resourceKey, context.path, arrayValue);
1106
+ if (peeked) {
1107
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1108
+ if (!test) {
1109
+ throw new Error(`Expected the peekManagedArray for ${context.field.kind} to return a ManagedArray`);
1110
+ }
1111
+ })(ARRAY_SIGNAL in peeked) : {};
1112
+ const arrSignal = peeked[ARRAY_SIGNAL];
1113
+ arrSignal.isStale = true;
1114
+ if (!Array.isArray(arrayValue)) {
1115
+ fieldSignal.value = null;
1116
+ }
665
1117
  }
666
- return new ResourceRelationship(store, cache, parent, identifier, field, prop, editable);
1118
+ return true;
667
1119
  }
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);
1120
+ function getSchemaObjectField(context) {
1121
+ const signal = entangleSignal(context.signals, context.record, context.path.at(-1), null);
1122
+ const {
1123
+ store,
1124
+ resourceKey,
1125
+ path,
1126
+ field
1127
+ } = context;
1128
+ const {
1129
+ cache
1130
+ } = store;
1131
+ let rawValue = context.editable ? cache.getAttr(resourceKey, path) : cache.getRemoteAttr(resourceKey, path);
1132
+ if (!rawValue && !field.options?.polymorphic && field.options?.defaultValue) {
1133
+ rawValue = {};
1134
+ }
1135
+ if (!rawValue) {
1136
+ if (signal.value) {
1137
+ const value = signal.value;
1138
+ // TODO if we had idle scheduling this should be done there.
1139
+ void Promise.resolve().then(() => {
1140
+ value.value[Destroy]();
1141
+ });
1142
+ signal.value = null;
1143
+ }
1144
+ return null;
679
1145
  }
680
- if (managedArray) {
681
- return managedArray;
682
- } else {
683
- const rawValue = cache.getRelationship(identifier, field.name);
684
- if (!rawValue) {
685
- return null;
1146
+ const {
1147
+ schema
1148
+ } = store;
1149
+ let objectType;
1150
+ if (field.options?.polymorphic) {
1151
+ const typePath = field.options.type ?? 'type';
1152
+ // if we are polymorphic, then context.field.options.type will
1153
+ // either specify a path on the rawValue to use as the type, defaulting to "type" or
1154
+ // the special string "@hash" which tells us to treat field.type as a hashFn name with which
1155
+ // to calc the type.
1156
+ if (typePath === '@hash') {
1157
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1158
+ if (!test) {
1159
+ throw new Error(`Expected the field to define a hashFn as its type`);
1160
+ }
1161
+ })(field.type) : {};
1162
+ const hashFn = schema.hashFn({
1163
+ type: field.type
1164
+ });
1165
+ // TODO consider if there are better options and name args we could provide.
1166
+ objectType = hashFn(rawValue, null, null);
1167
+ } else {
1168
+ objectType = rawValue[typePath];
1169
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1170
+ if (!test) {
1171
+ throw new Error(`Expected the type path for the field to be a value on the raw object`);
1172
+ }
1173
+ })(typePath && objectType && typeof objectType === 'string') : {};
686
1174
  }
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]]));
1175
+ } else {
1176
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1177
+ if (!test) {
1178
+ throw new Error(`A non-polymorphic SchemaObjectField must provide a SchemaObject type in its definition`);
1179
+ }
1180
+ })(field.type) : {};
1181
+ objectType = field.type;
1182
+ }
1183
+ const hashField = schema.resource({
1184
+ type: objectType
1185
+ }).identity;
1186
+ const identity = hashField ? schema.hashFn(hashField)(rawValue, hashField.options ?? null, hashField.name) : field.name;
1187
+ const cachedSchemaObject = signal.value;
1188
+ if (cachedSchemaObject) {
1189
+ if (cachedSchemaObject.type === objectType && cachedSchemaObject.identity === identity) {
1190
+ return cachedSchemaObject.value;
710
1191
  } else {
711
- managedArrayMapForRecord.set(field.name, managedArray);
1192
+ // TODO if we had idle scheduling this should be done there.
1193
+ void Promise.resolve().then(() => {
1194
+ cachedSchemaObject.value[Destroy]();
1195
+ });
712
1196
  }
713
1197
  }
714
- return managedArray;
1198
+ const schemaObject = new ReactiveResource({
1199
+ store: context.store,
1200
+ resourceKey: context.resourceKey,
1201
+ modeName: context.modeName,
1202
+ legacy: context.legacy,
1203
+ editable: context.editable,
1204
+ path: context.path,
1205
+ field: context.field,
1206
+ value: objectType
1207
+ });
1208
+ signal.value = {
1209
+ type: objectType,
1210
+ identity: identity,
1211
+ value: schemaObject
1212
+ };
1213
+ return schemaObject;
715
1214
  }
1215
+ function setSchemaObjectField(context) {
1216
+ const {
1217
+ store,
1218
+ value
1219
+ } = context;
1220
+ let newValue = value;
1221
+ if (value !== null) {
1222
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1223
+ if (!test) {
1224
+ throw new Error(`Expected value to be an object`);
1225
+ }
1226
+ })(typeof value === 'object') : {};
1227
+ newValue = {
1228
+ ...value
1229
+ };
1230
+ // FIXME the case of field.type to string here is likely incorrect
1231
+ const schemaFields = store.schema.fields({
1232
+ type: context.field.type
1233
+ });
1234
+ for (const key of Object.keys(newValue)) {
1235
+ if (!schemaFields.has(key)) {
1236
+ throw new Error(`Field ${key} does not exist on schema object ${context.field.type}`);
1237
+ }
1238
+ }
1239
+ } else {
1240
+ ManagedObjectMap.delete(context.record);
1241
+ }
1242
+ store.cache.setAttr(context.resourceKey, context.path, newValue);
1243
+ // const peeked = peekManagedObject(self, field);
1244
+ // if (peeked) {
1245
+ // const objSignal = peeked[OBJECT_SIGNAL];
1246
+ // objSignal.isStale = true;
1247
+ // }
1248
+ return true;
1249
+ }
1250
+ const DefaultMode = {
1251
+ '@hash': {
1252
+ get: getHashField,
1253
+ set: setHashField,
1254
+ mutable: false,
1255
+ enumerable: false,
1256
+ serializable: false
1257
+ },
1258
+ '@id': {
1259
+ get: getIdentityField,
1260
+ set: setIdentityField,
1261
+ mutable: true,
1262
+ enumerable: true,
1263
+ serializable: true
1264
+ },
1265
+ '@local': {
1266
+ get: getLocalField,
1267
+ set: setLocalField,
1268
+ mutable: true,
1269
+ enumerable: false,
1270
+ serializable: false
1271
+ },
1272
+ alias: {
1273
+ get: getAliasField,
1274
+ set: setAliasField,
1275
+ mutable: true,
1276
+ enumerable: true,
1277
+ serializable: false
1278
+ },
1279
+ array: {
1280
+ get: getArrayField,
1281
+ set: setArrayField,
1282
+ mutable: true,
1283
+ enumerable: true,
1284
+ serializable: true
1285
+ },
1286
+ attribute: {
1287
+ get: getAttributeField,
1288
+ set: setAttributeField,
1289
+ mutable: true,
1290
+ enumerable: true,
1291
+ serializable: true
1292
+ },
1293
+ belongsTo: {
1294
+ get: getBelongsToField,
1295
+ set: setBelongsToField,
1296
+ mutable: true,
1297
+ enumerable: true,
1298
+ serializable: true
1299
+ },
1300
+ collection: {
1301
+ get: getCollectionField,
1302
+ set: setCollectionField,
1303
+ mutable: true,
1304
+ enumerable: true,
1305
+ serializable: true
1306
+ },
1307
+ derived: {
1308
+ get: getDerivedField,
1309
+ set: setDerivedField,
1310
+ mutable: true,
1311
+ enumerable: true,
1312
+ serializable: false
1313
+ },
1314
+ field: {
1315
+ get: getGenericField,
1316
+ set: setGenericField,
1317
+ mutable: true,
1318
+ enumerable: true,
1319
+ serializable: true
1320
+ },
1321
+ hasMany: {
1322
+ get: getHasManyField,
1323
+ set: setHasManyField,
1324
+ mutable: true,
1325
+ enumerable: true,
1326
+ serializable: true
1327
+ },
1328
+ object: {
1329
+ get: getObjectField,
1330
+ set: setObjectField,
1331
+ mutable: true,
1332
+ enumerable: true,
1333
+ serializable: true
1334
+ },
1335
+ resource: {
1336
+ get: getResourceField,
1337
+ set: setResourceField,
1338
+ mutable: true,
1339
+ enumerable: true,
1340
+ serializable: true
1341
+ },
1342
+ 'schema-array': {
1343
+ get: getArrayField,
1344
+ set: setSchemaArrayField,
1345
+ mutable: true,
1346
+ enumerable: true,
1347
+ serializable: true
1348
+ },
1349
+ 'schema-object': {
1350
+ get: getSchemaObjectField,
1351
+ set: setSchemaObjectField,
1352
+ mutable: true,
1353
+ enumerable: true,
1354
+ serializable: true
1355
+ }
1356
+ };
716
1357
  const IgnoredGlobalFields = new Set(['length', 'nodeType', 'then', 'setInterval', 'document', STRUCTURED]);
717
- const symbolList = [Destroy, RecordStore, Identifier, Editable, Parent, Checkout, Legacy, EmbeddedPath, EmbeddedField];
1358
+ const symbolList = [Context, Destroy, RecordStore, Checkout];
718
1359
  const RecordSymbols = new Set(symbolList);
719
1360
  function isPathMatch(a, b) {
720
1361
  return a.length === b.length && a.every((v, i) => v === b[i]);
@@ -737,27 +1378,34 @@ const Editables = new WeakMap();
737
1378
  */
738
1379
  // eslint-disable-next-line @typescript-eslint/no-extraneous-class
739
1380
  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;
743
- this[RecordStore] = store;
744
- if (isEmbedded) {
745
- this[Parent] = identifier;
746
- } else {
747
- this[Identifier] = identifier;
748
- }
749
- const IS_EDITABLE = this[Editable] = Mode[Editable] ?? false;
750
- this[Legacy] = Mode[Legacy] ?? false;
1381
+ constructor(context) {
1382
+ const {
1383
+ store
1384
+ } = context;
1385
+ const identifier = context.resourceKey;
1386
+ const embeddedField = context.field;
1387
+ const embeddedPath = context.path;
1388
+ const isEmbedded = context.field !== null;
1389
+ const IS_EDITABLE = context.editable ?? false;
751
1390
  const schema = store.schema;
752
- const cache = store.cache;
753
- const identityField = schema.resource(isEmbedded ? embeddedField : identifier).identity;
1391
+ const objectType = isEmbedded ? context.value : identifier.type;
1392
+ const ResourceSchema = schema.resource(isEmbedded ? {
1393
+ type: objectType
1394
+ } : identifier);
1395
+ const identityField = ResourceSchema.identity;
754
1396
  const BoundFns = new Map();
755
1397
 
756
1398
  // prettier-ignore
757
- const extensions = !Mode[Legacy] ? null : isEmbedded ? schema.CAUTION_MEGA_DANGER_ZONE_objectExtensions(embeddedField) : schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(identifier);
758
- this[EmbeddedField] = embeddedField;
759
- this[EmbeddedPath] = embeddedPath;
760
- const fields = isEmbedded ? schema.fields(embeddedField) : schema.fields(identifier);
1399
+ const extensions = !context.legacy ? null : isEmbedded ? schema.CAUTION_MEGA_DANGER_ZONE_objectExtensions(embeddedField, objectType) : schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(identifier);
1400
+ this[Context] = context;
1401
+ this[RecordStore] = context.store;
1402
+ const fields = isEmbedded ? schema.fields({
1403
+ type: objectType
1404
+ }) : schema.fields(identifier);
1405
+ const method = typeof schema.cacheFields === 'function' ? 'cacheFields' : 'fields';
1406
+ const cacheFields = isEmbedded ? schema[method]({
1407
+ type: objectType
1408
+ }) : schema[method](identifier);
761
1409
  const signals = withSignalStore(this);
762
1410
  const proxy = new Proxy(this, {
763
1411
  ownKeys() {
@@ -934,74 +1582,64 @@ class ReactiveResource {
934
1582
  throw new Error(`Alias fields cannot alias '@id' '@local' '@hash' or 'derived' fields`);
935
1583
  }
936
1584
  })(maybeField.kind !== 'alias' || !['@id', '@local', '@hash', 'derived'].includes(maybeField.options.kind)) : {};
1585
+ /**
1586
+ * Prop Array is the path from a resource to the field including
1587
+ * intermediate "links" on arrays,objects,schema-arrays and schema-objects.
1588
+ *
1589
+ * E.g. in the following
1590
+ *
1591
+ * ```
1592
+ * const user = {
1593
+ * addresses: [{
1594
+ * street: 'Sunset Blvd',
1595
+ * zip: 90210
1596
+ * }]
1597
+ * }
1598
+ * ```
1599
+ *
1600
+ * The propArray for "street" is ['addresses', 0, 'street']
1601
+ *
1602
+ * Prop Array follows the `cache` path to the value, not the ui path.
1603
+ * Thus, if `addresses` has a sourceKey of `user_addresses` and
1604
+ * `zip` has a sourceKey of `zip_code` then the propArray for "zip" is
1605
+ * ['user_addresses', 0, 'zip_code']
1606
+ */
937
1607
  const propArray = isEmbedded ? embeddedPath.slice() : [];
938
1608
  // we use the field.name instead of prop here because we want to use the cache-path not
939
1609
  // the record path.
940
- propArray.push(field.name);
941
- // propArray.push(prop as string);
942
-
1610
+ // SAFETY: we lie as string here because if we were to get null
1611
+ // we would be in a field kind that won't use the propArray below.
1612
+ const fieldCacheKey = getFieldCacheKey(field);
1613
+ propArray.push(fieldCacheKey);
943
1614
  switch (field.kind) {
944
1615
  case '@id':
945
- entangleSignal(signals, receiver, '@identity', null);
946
- return identifier.id;
947
1616
  case '@hash':
948
- // TODO pass actual cache value not {}
949
- return schema.hashFn(field)({}, field.options ?? null, field.name ?? null);
950
1617
  case '@local':
951
- {
952
- return computeLocal(receiver, field, prop);
953
- }
1618
+ case 'derived':
954
1619
  case 'field':
955
- entangleSignal(signals, receiver, field.name, null);
956
- return computeField(schema, cache, target, identifier, field, propArray, IS_EDITABLE);
957
1620
  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
1621
  case 'schema-array':
966
1622
  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
1623
  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]);
1624
+ case 'object':
1625
+ case 'resource':
976
1626
  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
1627
  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);
1628
+ case 'collection':
1629
+ return DefaultMode[field.kind].get({
1630
+ store,
1631
+ resourceKey: identifier,
1632
+ modeName: context.modeName,
1633
+ legacy: context.legacy,
1634
+ editable: context.editable,
1635
+ path: propArray,
1636
+ field: field,
1637
+ record: receiver,
1638
+ signals,
1639
+ value: null
1640
+ });
1003
1641
  default:
1004
- throw new Error(`Field '${String(prop)}' on '${identifier.type}' has the unknown kind '${field.kind}'`);
1642
+ assertNeverField(identifier, field, propArray);
1005
1643
  }
1006
1644
  },
1007
1645
  set(target, prop, value, receiver) {
@@ -1028,203 +1666,64 @@ class ReactiveResource {
1028
1666
  throw new Error(`Alias fields cannot alias '@id' '@local' '@hash' or 'derived' fields`);
1029
1667
  }
1030
1668
  })(maybeField.kind !== 'alias' || !['@id', '@local', '@hash', 'derived'].includes(maybeField.options.kind)) : {};
1669
+ /**
1670
+ * Prop Array is the path from a resource to the field including
1671
+ * intermediate "links" on arrays,objects,schema-arrays and schema-objects.
1672
+ *
1673
+ * E.g. in the following
1674
+ *
1675
+ * ```
1676
+ * const user = {
1677
+ * addresses: [{
1678
+ * street: 'Sunset Blvd',
1679
+ * zip: 90210
1680
+ * }]
1681
+ * }
1682
+ * ```
1683
+ *
1684
+ * The propArray for "street" is ['addresses', 0, 'street']
1685
+ *
1686
+ * Prop Array follows the `cache` path to the value, not the ui path.
1687
+ * Thus, if `addresses` has a sourceKey of `user_addresses` and
1688
+ * `zip` has a sourceKey of `zip_code` then the propArray for "zip" is
1689
+ * ['user_addresses', 0, 'zip_code']
1690
+ */
1031
1691
  const propArray = isEmbedded ? embeddedPath.slice() : [];
1032
1692
  // we use the field.name instead of prop here because we want to use the cache-path not
1033
1693
  // the record path.
1034
- propArray.push(field.name);
1035
- // propArray.push(prop as string);
1036
-
1694
+ // SAFETY: we lie as string here because if we were to get null
1695
+ // we would be in a field kind that won't use the propArray below.
1696
+ const fieldCacheKey = getFieldCacheKey(field);
1697
+ propArray.push(fieldCacheKey);
1037
1698
  switch (field.kind) {
1038
1699
  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
- }
1700
+ case '@hash':
1058
1701
  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
1702
  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
1703
  case 'attribute':
1079
- {
1080
- cache.setAttr(identifier, propArray, value);
1081
- return true;
1082
- }
1704
+ case 'derived':
1083
1705
  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
1706
  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
1707
  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
- }
1708
+ case 'object':
1709
+ case 'resource':
1205
1710
  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
1711
  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;
1712
+ case 'collection':
1713
+ return DefaultMode[field.kind].set({
1714
+ store,
1715
+ resourceKey: identifier,
1716
+ modeName: context.modeName,
1717
+ legacy: context.legacy,
1718
+ editable: context.editable,
1719
+ path: propArray,
1720
+ field: field,
1721
+ record: receiver,
1722
+ signals,
1723
+ value
1724
+ });
1226
1725
  default:
1227
- throw new Error(`Unknown field kind ${field.kind}`);
1726
+ return assertNeverField(identifier, field, propArray);
1228
1727
  }
1229
1728
  }
1230
1729
  });
@@ -1255,25 +1754,25 @@ class ReactiveResource {
1255
1754
  // TODO we should likely handle this notification here
1256
1755
  // also we should add a LOGGING flag
1257
1756
  // eslint-disable-next-line no-console
1258
- console.warn(`Notification unhandled for ${key.join(',')} on ${identifier.type}`, self);
1757
+ console.warn(`Notification unhandled for ${key.join(',')} on ${identifier.type}`, proxy);
1259
1758
  return;
1260
1759
  }
1261
1760
 
1262
1761
  // TODO we should add a LOGGING flag
1263
- // console.log(`Deep notification skipped for ${key.join('.')} on ${identifier.type}`, self);
1762
+ // console.log(`Deep notification skipped for ${key.join('.')} on ${identifier.type}`, proxy);
1264
1763
  // deep notify the key path
1265
1764
  } else {
1266
1765
  if (isEmbedded) return; // base paths never apply to embedded records
1267
1766
 
1268
1767
  // TODO determine what LOGGING flag to wrap this in if any
1269
- // console.log(`Notification for ${key} on ${identifier.type}`, self);
1768
+ // console.log(`Notification for ${key} on ${identifier.type}`, proxy);
1270
1769
  const signal = signals.get(key);
1271
1770
  if (signal) {
1272
1771
  notifyInternalSignal(signal);
1273
1772
  }
1274
- const field = fields.get(key);
1773
+ const field = cacheFields.get(key);
1275
1774
  if (field?.kind === 'array' || field?.kind === 'schema-array') {
1276
- const peeked = peekManagedArray(self, field);
1775
+ const peeked = signal?.value;
1277
1776
  if (peeked) {
1278
1777
  macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1279
1778
  if (!test) {
@@ -1285,7 +1784,7 @@ class ReactiveResource {
1285
1784
  }
1286
1785
  }
1287
1786
  if (field?.kind === 'object') {
1288
- const peeked = peekManagedObject(self, field);
1787
+ const peeked = peekManagedObject(proxy, field);
1289
1788
  if (peeked) {
1290
1789
  const objSignal = peeked[OBJECT_SIGNAL];
1291
1790
  notifyInternalSignal(objSignal);
@@ -1299,15 +1798,15 @@ class ReactiveResource {
1299
1798
  if (Array.isArray(key)) ;else {
1300
1799
  if (isEmbedded) return; // base paths never apply to embedded records
1301
1800
 
1302
- const field = fields.get(key);
1801
+ const field = cacheFields.get(key);
1303
1802
  macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1304
1803
  if (!test) {
1305
- throw new Error(`Expected relationshp ${key} to be the name of a field`);
1804
+ throw new Error(`Expected relationship ${key} to be the name of a field`);
1306
1805
  }
1307
1806
  })(field) : {};
1308
1807
  if (field.kind === 'belongsTo') {
1309
1808
  // TODO determine what LOGGING flag to wrap this in if any
1310
- // console.log(`Notification for ${key} on ${identifier.type}`, self);
1809
+ // console.log(`Notification for ${key} on ${identifier.type}`, proxy);
1311
1810
  const signal = signals.get(key);
1312
1811
  if (signal) {
1313
1812
  notifyInternalSignal(signal);
@@ -1315,9 +1814,12 @@ class ReactiveResource {
1315
1814
  // FIXME
1316
1815
  } else if (field.kind === 'resource') ;else if (field.kind === 'hasMany') {
1317
1816
  if (field.options.linksMode) {
1318
- const peeked = peekManagedArray(self, field);
1319
- if (peeked) {
1320
- notifyInternalSignal(peeked[ARRAY_SIGNAL]);
1817
+ const signal = signals.get(key);
1818
+ if (signal) {
1819
+ const peeked = signal.value;
1820
+ if (peeked) {
1821
+ notifyInternalSignal(peeked[ARRAY_SIGNAL]);
1822
+ }
1321
1823
  }
1322
1824
  return;
1323
1825
  }
@@ -1325,7 +1827,7 @@ class ReactiveResource {
1325
1827
  if (!test) {
1326
1828
  throw new Error(`Can only use hasMany fields when the resource is in legacy mode`);
1327
1829
  }
1328
- })(Mode[Legacy]) : {};
1830
+ })(context.legacy) : {};
1329
1831
  if (schema._kind('@legacy', 'hasMany').notify(store, proxy, identifier, field)) {
1330
1832
  macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1331
1833
  if (!test) {
@@ -1367,35 +1869,52 @@ class ReactiveResource {
1367
1869
  }
1368
1870
  }
1369
1871
  function _CHECKOUT(record) {
1872
+ const context = record[Context];
1873
+
1370
1874
  // IF we are already the editable record, throw an error
1371
- if (record[Editable]) {
1875
+ if (context.editable) {
1372
1876
  throw new Error(`Cannot checkout an already editable record`);
1373
1877
  }
1374
1878
  const editable = Editables.get(record);
1375
1879
  if (editable) {
1376
1880
  return Promise.resolve(editable);
1377
1881
  }
1378
- const embeddedType = record[EmbeddedField];
1379
- const embeddedPath = record[EmbeddedPath];
1380
- const isEmbedded = embeddedType !== null && embeddedPath !== null;
1882
+ const isEmbedded = context.field !== null && context.path !== null;
1381
1883
  if (isEmbedded) {
1382
1884
  throw new Error(`Cannot checkout an embedded record (yet)`);
1383
1885
  }
1384
- const editableRecord = new ReactiveResource(record[RecordStore], record[Identifier], {
1385
- [Editable]: true,
1386
- [Legacy]: record[Legacy]
1387
- }, isEmbedded, embeddedType, embeddedPath);
1886
+ const editableRecord = new ReactiveResource({
1887
+ store: context.store,
1888
+ resourceKey: context.resourceKey,
1889
+ modeName: context.legacy ? 'legacy' : 'polaris',
1890
+ legacy: context.legacy,
1891
+ editable: true,
1892
+ path: null,
1893
+ field: null,
1894
+ value: null
1895
+ });
1388
1896
  setRecordIdentifier(editableRecord, recordIdentifierFor(record));
1389
1897
  return Promise.resolve(editableRecord);
1390
1898
  }
1391
1899
  function _DESTROY(record) {
1392
- if (record[Legacy]) {
1900
+ if (record[Context].legacy) {
1393
1901
  // @ts-expect-error
1394
1902
  record.isDestroying = true;
1395
1903
  // @ts-expect-error
1396
1904
  record.isDestroyed = true;
1397
1905
  }
1398
- record[RecordStore].notifications.unsubscribe(record.___notifications);
1906
+ record[Context].store.notifications.unsubscribe(record.___notifications);
1907
+
1908
+ // FIXME we need a way to also unsubscribe all SchemaObjects when the primary
1909
+ // resource is destroyed.
1910
+ }
1911
+ function assertNeverField(identifier, field, path) {
1912
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1913
+ {
1914
+ throw new Error(`Cannot use unknown field kind ${field.kind} on <${identifier.type}>.${Array.isArray(path) ? path.join('.') : path}`);
1915
+ }
1916
+ })() : {};
1917
+ return false;
1399
1918
  }
1400
1919
  function instantiateRecord(store, identifier, createArgs) {
1401
1920
  const schema = store.schema;
@@ -1405,11 +1924,17 @@ function instantiateRecord(store, identifier, createArgs) {
1405
1924
  throw new Error(`Expected a resource schema`);
1406
1925
  }
1407
1926
  })(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
1927
+ const legacy = resourceSchema?.legacy ?? false;
1928
+ const editable = legacy || store.cache.isNew(identifier);
1929
+ const record = new ReactiveResource({
1930
+ store,
1931
+ resourceKey: identifier,
1932
+ modeName: legacy ? 'legacy' : 'polaris',
1933
+ legacy: legacy,
1934
+ editable: editable,
1935
+ path: null,
1936
+ field: null,
1937
+ value: null
1413
1938
  });
1414
1939
  if (createArgs) {
1415
1940
  Object.assign(record, createArgs);
@@ -1546,7 +2071,7 @@ function getExt(extCache, type, extName) {
1546
2071
  function hasObjectSchema(field) {
1547
2072
  return 'kind' in field && (field.kind === 'schema-array' || field.kind === 'schema-object');
1548
2073
  }
1549
- function processExtensions(schema, field, scenario) {
2074
+ function processExtensions(schema, field, scenario, resolvedType) {
1550
2075
  // if we're looking up extensions for a resource, there is no
1551
2076
  // merging required so if we have no objectExtensions
1552
2077
  // we are done.
@@ -1573,12 +2098,16 @@ function processExtensions(schema, field, scenario) {
1573
2098
  if (!hasObjectSchema(field)) {
1574
2099
  return null;
1575
2100
  }
1576
- return schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(field);
2101
+ return schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(resolvedType ? {
2102
+ type: resolvedType
2103
+ } : field);
1577
2104
  }
1578
2105
 
1579
2106
  // if we have made it here, we have extensions, lets check if there's
1580
2107
  // 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;
2108
+ const baseExtensions = scenario === 'resource' && hasObjectSchema(field) ? schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(field) : scenario === 'object' && hasObjectSchema(field) ? schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(resolvedType ? {
2109
+ type: resolvedType
2110
+ } : field) : null;
1582
2111
  if (!baseExtensions && extensions.length === 1) {
1583
2112
  const value = getExt(extCache, type, extensions[0]);
1584
2113
  fieldCache[type].set(field, value);
@@ -1646,7 +2175,8 @@ function withDefaults(schema) {
1646
2175
  * @public
1647
2176
  */
1648
2177
  const fromIdentity = (record, options, key) => {
1649
- const identifier = record[Identifier];
2178
+ const context = record[Context];
2179
+ const identifier = context.resourceKey;
1650
2180
  macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1651
2181
  if (!test) {
1652
2182
  throw new Error(`Cannot compute @identity for a record without an identifier`);
@@ -1836,16 +2366,21 @@ class SchemaService {
1836
2366
  relationships[field.name] = field;
1837
2367
  }
1838
2368
  }
2369
+ const cacheFields = null;
1839
2370
  const traits = new Set(isResourceSchema(schema) ? schema.traits : []);
1840
2371
  const finalized = traits.size === 0;
1841
2372
  const internalSchema = {
1842
2373
  original: schema,
1843
2374
  finalized,
1844
2375
  fields,
2376
+ cacheFields,
1845
2377
  relationships,
1846
2378
  attributes,
1847
2379
  traits
1848
2380
  };
2381
+ if (traits.size === 0) {
2382
+ internalSchema.cacheFields = getCacheFields(internalSchema);
2383
+ }
1849
2384
  this._schemas.set(schema.type, internalSchema);
1850
2385
  }
1851
2386
 
@@ -1902,13 +2437,13 @@ class SchemaService {
1902
2437
  }
1903
2438
  CAUTION_MEGA_DANGER_ZONE_resourceExtensions(resource) {
1904
2439
  const schema = this.resource(resource);
1905
- return processExtensions(this, schema, 'resource');
2440
+ return processExtensions(this, schema, 'resource', null);
1906
2441
  }
1907
- CAUTION_MEGA_DANGER_ZONE_objectExtensions(field) {
1908
- return processExtensions(this, field, 'object');
2442
+ CAUTION_MEGA_DANGER_ZONE_objectExtensions(field, resolvedType) {
2443
+ return processExtensions(this, field, 'object', resolvedType);
1909
2444
  }
1910
2445
  CAUTION_MEGA_DANGER_ZONE_arrayExtensions(field) {
1911
- return processExtensions(this, field, 'array');
2446
+ return processExtensions(this, field, 'array', null);
1912
2447
  }
1913
2448
  CAUTION_MEGA_DANGER_ZONE_hasExtension(ext) {
1914
2449
  return this._extensions[ext.kind].has(ext.name);
@@ -1976,6 +2511,20 @@ class SchemaService {
1976
2511
  }
1977
2512
  return schema.fields;
1978
2513
  }
2514
+ cacheFields({
2515
+ type
2516
+ }) {
2517
+ const schema = this._schemas.get(type);
2518
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
2519
+ if (!test) {
2520
+ throw new Error(`No schema defined for ${type}`);
2521
+ }
2522
+ })(schema) : {};
2523
+ if (!schema.finalized) {
2524
+ finalizeResource(this, schema);
2525
+ }
2526
+ return schema.cacheFields;
2527
+ }
1979
2528
  hasResource(resource) {
1980
2529
  return this._schemas.has(resource.type);
1981
2530
  }
@@ -2064,8 +2613,27 @@ function finalizeResource(schema, resource) {
2064
2613
  }
2065
2614
  mergeMap(fields, resource.fields);
2066
2615
  resource.fields = fields;
2616
+ resource.cacheFields = getCacheFields(resource);
2067
2617
  resource.finalized = true;
2068
2618
  }
2619
+ function getCacheFields(resource) {
2620
+ const {
2621
+ fields
2622
+ } = resource;
2623
+ const cacheFields = new Map();
2624
+ for (const [key, value] of fields) {
2625
+ if (isNonIdentityCacheableField(value)) {
2626
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
2627
+ if (!test) {
2628
+ 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`);
2629
+ }
2630
+ })(!value.sourceKey || value.sourceKey === key || !fields.has(value.sourceKey)) : {};
2631
+ const cacheKey = getFieldCacheKeyStrict(value);
2632
+ cacheFields.set(cacheKey, value);
2633
+ }
2634
+ }
2635
+ return cacheFields;
2636
+ }
2069
2637
  function walkTrait(schema, trait, fields, seen, type, debugPath) {
2070
2638
  if (seen.has(trait)) {
2071
2639
  // if the trait is in the current path, we throw a cycle error in dev.