Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement managed object for schemaRecord #9277

Merged
merged 3 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions packages/schema-record/src/managed-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import type Store from '@ember-data/store';
import type { FieldSchema } from '@ember-data/store/-types/q/schema-service';
import type { Signal } from '@ember-data/tracking/-private';
import { addToTransaction, createSignal, subscribe } from '@ember-data/tracking/-private';
import type { StableRecordIdentifier } from '@warp-drive/core-types';
import type { Cache } from '@warp-drive/core-types/cache';
import type { ObjectValue, Value } from '@warp-drive/core-types/json/raw';

import type { SchemaRecord } from './record';
import type { SchemaService } from './schema';

export const SOURCE = Symbol('#source');
export const MUTATE = Symbol('#update');
export const OBJECT_SIGNAL = Symbol('#signal');
export const NOTIFY = Symbol('#notify');

export function notifyObject(obj: ManagedObject) {
addToTransaction(obj[OBJECT_SIGNAL]);
}

type KeyType = string | symbol | number;

export interface ManagedObject {
[MUTATE]?(
target: unknown[],
receiver: typeof Proxy<unknown[]>,
prop: string,
args: unknown[],
_SIGNAL: Signal
): unknown;
}

export class ManagedObject {
[SOURCE]: object;
declare address: StableRecordIdentifier;
declare key: string;
declare owner: SchemaRecord;
declare [OBJECT_SIGNAL]: Signal;

constructor(
store: Store,
schema: SchemaService,
cache: Cache,
field: FieldSchema,
data: object,
address: StableRecordIdentifier,
key: string,
owner: SchemaRecord
) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
this[SOURCE] = { ...data };
this[OBJECT_SIGNAL] = createSignal(this, 'length');
const _SIGNAL = this[OBJECT_SIGNAL];
// const boundFns = new Map<KeyType, ProxiedMethod>();
this.address = address;
this.key = key;
this.owner = owner;
const transaction = false;

const proxy = new Proxy(this[SOURCE], {
get<R extends typeof Proxy<object>>(target: object, prop: keyof R, receiver: R) {
if (prop === OBJECT_SIGNAL) {
return _SIGNAL;
}
if (prop === 'address') {
return self.address;
}
if (prop === 'key') {
return self.key;
}
if (prop === 'owner') {
return self.owner;
}

if (_SIGNAL.shouldReset) {
_SIGNAL.t = false;
_SIGNAL.shouldReset = false;
let newData = cache.getAttr(self.address, self.key);
if (newData && newData !== self[SOURCE]) {
if (field.type !== null) {
const transform = schema.transforms.get(field.type);
if (!transform) {
throw new Error(`No '${field.type}' transform defined for use by ${address.type}.${String(prop)}`);
}
newData = transform.hydrate(newData as ObjectValue, field.options ?? null, self.owner) as ObjectValue;
}
self[SOURCE] = { ...(newData as ObjectValue) }; // Add type assertion for newData
}
}

if (prop in self[SOURCE]) {
if (!transaction) {
subscribe(_SIGNAL);
}

return (self[SOURCE] as R)[prop];
}
return Reflect.get(target, prop, receiver) as R;
},

set(target, prop: KeyType, value, receiver) {
if (prop === 'address') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
self.address = value;
return true;
}
if (prop === 'key') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
self.key = value;
return true;
}
if (prop === 'owner') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
self.owner = value;
return true;
}
const reflect = Reflect.set(target, prop, value, receiver);

if (reflect) {
if (field.type === null) {
cache.setAttr(self.address, self.key, self[SOURCE] as Value);
_SIGNAL.shouldReset = true;
return true;
}

const transform = schema.transforms.get(field.type);
if (!transform) {
throw new Error(`No '${field.type}' transform defined for use by ${address.type}.${String(prop)}`);
}
const val = transform.serialize(self[SOURCE], field.options ?? null, self.owner);
cache.setAttr(self.address, self.key, val);
_SIGNAL.shouldReset = true;
}
return reflect;
},
}) as ManagedObject;

return proxy;
}
}
97 changes: 96 additions & 1 deletion packages/schema-record/src/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ import {
import type { StableRecordIdentifier } from '@warp-drive/core-types';
import type { Cache } from '@warp-drive/core-types/cache';
import type { ResourceRelationship as SingleResourceRelationship } from '@warp-drive/core-types/cache/relationship';
import type { ArrayValue, Value } from '@warp-drive/core-types/json/raw';
import type { ArrayValue, ObjectValue, Value } from '@warp-drive/core-types/json/raw';
import { STRUCTURED } from '@warp-drive/core-types/request';
import type { Link, Links } from '@warp-drive/core-types/spec/raw';
import { RecordStore } from '@warp-drive/core-types/symbols';

import { ARRAY_SIGNAL, ManagedArray } from './managed-array';
import { ManagedObject, OBJECT_SIGNAL } from './managed-object';
import type { SchemaService } from './schema';

export const Destroy = Symbol('Destroy');
Expand All @@ -37,6 +38,7 @@ const IgnoredGlobalFields = new Set(['then', STRUCTURED]);
const RecordSymbols = new Set([Destroy, RecordStore, Identifier, Editable, Parent, Checkout, Legacy, Signals]);

const ManagedArrayMap = new Map<SchemaRecord, Map<FieldSchema, ManagedArray>>();
const ManagedObjectMap = new Map<SchemaRecord, Map<FieldSchema, ManagedObject>>();

function computeLocal(record: typeof Proxy<SchemaRecord>, field: FieldSchema, prop: string): unknown {
let signal = peekSignal(record, prop);
Expand All @@ -56,6 +58,13 @@ function peekManagedArray(record: SchemaRecord, field: FieldSchema): ManagedArra
}
}

function peekManagedObject(record: SchemaRecord, field: FieldSchema): ManagedObject | undefined {
const managedObjectMapForRecord = ManagedObjectMap.get(record);
if (managedObjectMapForRecord) {
return managedObjectMapForRecord.get(field);
}
}

function computeField(
schema: SchemaService,
cache: Cache,
Expand Down Expand Up @@ -112,6 +121,46 @@ function computeArray(
return managedArray;
}

function computeObject(
store: Store,
schema: SchemaService,
cache: Cache,
record: SchemaRecord,
identifier: StableRecordIdentifier,
field: FieldSchema,
prop: string
) {
const managedObjectMapForRecord = ManagedObjectMap.get(record);
let managedObject;
if (managedObjectMapForRecord) {
managedObject = managedObjectMapForRecord.get(field);
}
if (managedObject) {
return managedObject;
} else {
let rawValue = cache.getAttr(identifier, prop) as object;
if (!rawValue) {
return null;
}
if (field.kind === 'object') {
if (field.type !== null) {
const transform = schema.transforms.get(field.type);
if (!transform) {
throw new Error(`No '${field.type}' transform defined for use by ${identifier.type}.${String(prop)}`);
}
rawValue = transform.hydrate(rawValue as ObjectValue, field.options ?? null, record) as object;
}
}
managedObject = new ManagedObject(store, schema, cache, field, rawValue, identifier, prop, record);
if (!managedObjectMapForRecord) {
ManagedObjectMap.set(record, new Map([[field, managedObject]]));
} else {
managedObjectMapForRecord.set(field, managedObject);
}
}
return managedObject;
}

function computeAttribute(cache: Cache, identifier: StableRecordIdentifier, prop: string): unknown {
return cache.getAttr(identifier, prop);
}
Expand Down Expand Up @@ -323,13 +372,27 @@ export class SchemaRecord {
return computeResource(store, cache, target, identifier, field, prop as string);
case 'derived':
return computeDerivation(schema, receiver as unknown as SchemaRecord, identifier, field, prop as string);
case 'schema-array':
throw new Error(`Not Implemented`);
case 'array':
assert(
`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`,
!target[Legacy]
);
entangleSignal(signals, receiver, field.name);
return computeArray(store, schema, cache, target, identifier, field, prop as string);
case 'schema-object':
// validate any access off of schema, no transform to run
// use raw cache value as the object to manage
throw new Error(`Not Implemented`);
case 'object':
assert(
`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`,
!target[Legacy]
);
entangleSignal(signals, receiver, field.name);
// run transform, then use that value as the object to manage
return computeObject(store, schema, cache, target, identifier, field, prop as string);
default:
throw new Error(`Field '${String(prop)}' on '${identifier.type}' has the unknown kind '${field.kind}'`);
}
Expand Down Expand Up @@ -402,6 +465,38 @@ export class SchemaRecord {
}
return true;
}
case 'object': {
if (field.type === null) {
let newValue = value;
if (value !== null) {
newValue = { ...(value as ObjectValue) };
} else {
ManagedObjectMap.delete(target);
}

cache.setAttr(identifier, prop as string, newValue as Value);

const peeked = peekManagedObject(self, field);
if (peeked) {
const objSignal = peeked[OBJECT_SIGNAL];
objSignal.shouldReset = true;
}
return true;
}
const transform = schema.transforms.get(field.type);
if (!transform) {
throw new Error(`No '${field.type}' transform defined for use by ${identifier.type}.${String(prop)}`);
}
const rawValue = transform.serialize({ ...(value as ObjectValue) }, field.options ?? null, target);

cache.setAttr(identifier, prop as string, rawValue);
const peeked = peekManagedObject(self, field);
if (peeked) {
const objSignal = peeked[OBJECT_SIGNAL];
objSignal.shouldReset = true;
}
return true;
}
case 'derived': {
throw new Error(`Cannot set ${String(prop)} on ${identifier.type} because it is derived`);
}
Expand Down
7 changes: 6 additions & 1 deletion packages/schema-record/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,12 @@ export class SchemaService {
kind: field.kind === 'resource' ? 'belongsTo' : 'hasMany',
}) as unknown as RelationshipSchema;
fieldSpec.relationships[field.name] = relSchema;
} else if (field.kind !== 'derived' && field.kind !== '@local' && field.kind !== 'array') {
} else if (
field.kind !== 'derived' &&
field.kind !== '@local' &&
field.kind !== 'array' &&
field.kind !== 'object'
) {
throw new Error(`Unknown field kind ${field.kind}`);
}
});
Expand Down
2 changes: 2 additions & 0 deletions packages/store/src/-types/q/schema-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ export interface FieldSchema {
| 'collection'
| 'derived'
| 'object'
| 'schema-object'
| 'array'
| 'schema-array'
| '@id'
| '@local';
options?: Record<string, unknown>;
Expand Down
Loading
Loading