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(private): reactive simple fields #8948

Merged
merged 1 commit into from
Oct 2, 2023
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
20 changes: 19 additions & 1 deletion packages/schema-record/src/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import type Store from '@ember-data/store';
import type { StableRecordIdentifier } from "@ember-data/types/q/identifier";
import type { FieldSchema, SchemaService } from './schema';
import { Cache } from '@ember-data/types/q/cache';
import { Document } from '@ember-data/store/-private/document';
import { tracked } from '@glimmer/tracking';
import { Link, Links, SingleResourceRelationship } from '@ember-data/types/q/ember-data-json-api';
import { StoreRequestInput } from '@ember-data/store/-private/cache-handler';
import { Future } from '@ember-data/request';
import { DEBUG } from '@ember-data/env';
import { NotificationType } from '@ember-data/store/-private/managers/notification-manager';
import { addToTransaction, entangleSignal } from '@ember-data/tracking/-private';

export const Destroy = Symbol('Destroy');
export const RecordStore = Symbol('Store');
Expand Down Expand Up @@ -110,6 +111,7 @@ export class SchemaRecord {
declare [RecordStore]: Store;
declare [Identifier]: StableRecordIdentifier;
declare [Editable]: boolean;
declare ___notifications: unknown;

constructor(store: Store, identifier: StableRecordIdentifier, editable: boolean) {
this[RecordStore] = store;
Expand All @@ -120,6 +122,20 @@ export class SchemaRecord {
const cache = store.cache;
const fields = schema.fields(identifier);

const signals = new Map();
this.___notifications = store.notifications.subscribe(identifier, (_: StableRecordIdentifier, type: NotificationType, key?: string) => {
switch (type) {
case 'attributes':
if (key) {
const signal = signals.get(key);
if (signal) {
addToTransaction(signal);
}
}
break;
}
});

return new Proxy(this, {
get(target, prop, receiver) {
if (prop === Destroy) {
Expand All @@ -140,8 +156,10 @@ export class SchemaRecord {

switch (field.kind) {
case 'attribute':
entangleSignal(signals, this, field.name);
return computeAttribute(schema, cache, target, identifier, field, prop as string);
case 'resource':
entangleSignal(signals, this, field.name);
return computeResource(store, cache, target, identifier, field, prop as string);

case 'derived':
Expand Down
3 changes: 2 additions & 1 deletion packages/tracking/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"dependencies": {
"@ember-data/private-build-infra": "workspace:5.5.0-alpha.10",
"@embroider/macros": "^1.13.1",
"ember-cli-babel": "^8.1.0"
"ember-cli-babel": "^8.1.0",
"@glimmer/validator": "^0.84.3"
},
"files": [
"addon-main.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/tracking/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default {
// You can augment this if you need to.
output: addon.output(),

external: ['@embroider/macros'],
external: ['@embroider/macros', '@glimmer/validator'],

plugins: [
// These are the modules that users should be able to import from your
Expand Down
81 changes: 70 additions & 11 deletions packages/tracking/src/-private.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { tagForProperty } from '@ember/-internals/metal';
import { consumeTag, dirtyTag } from '@glimmer/validator';

import { DEBUG } from '@ember-data/env';

/**
Expand All @@ -18,8 +21,8 @@ type OpaqueFn = (...args: unknown[]) => unknown;
type Tag = { ref: null; t: boolean };
type Transaction = {
cbs: Set<OpaqueFn>;
props: Set<Tag>;
sub: Set<Tag>;
props: Set<Tag | Signal>;
sub: Set<Tag | Signal>;
parent: Transaction | null;
};
let TRANSACTION: Transaction | null = null;
Expand All @@ -37,18 +40,26 @@ function createTransaction() {
TRANSACTION = transaction;
}

export function subscribe(obj: Tag): void {
export function subscribe(obj: Tag | Signal): void {
if (TRANSACTION) {
TRANSACTION.sub.add(obj);
} else if ('tag' in obj) {
// @ts-expect-error - we are using Ember's Tag not Glimmer's
consumeTag(obj.tag);
} else {
obj.ref;
}
}

function updateRef(obj: Tag): void {
function updateRef(obj: Tag | Signal): void {
if (DEBUG) {
try {
obj.ref = null;
if ('tag' in obj) {
// @ts-expect-error - we are using Ember's Tag not Glimmer's
dirtyTag(obj.tag);
} else {
obj.ref = null;
}
} catch (e: unknown) {
if (e instanceof Error) {
if (e.message.includes('You attempted to update `ref` on `Tag`')) {
Expand Down Expand Up @@ -97,7 +108,12 @@ function updateRef(obj: Tag): void {
throw e;
}
} else {
obj.ref = null;
if ('tag' in obj) {
// @ts-expect-error - we are using Ember's Tag not Glimmer's
dirtyTag(obj.tag);
} else {
obj.ref = null;
}
}
}

Expand All @@ -107,13 +123,18 @@ function flushTransaction() {
transaction.cbs.forEach((cb) => {
cb();
});
transaction.props.forEach((obj: Tag) => {
transaction.props.forEach((obj) => {
// mark this mutation as part of a transaction
obj.t = true;
updateRef(obj);
});
transaction.sub.forEach((obj: Tag) => {
obj.ref;
transaction.sub.forEach((obj) => {
if ('tag' in obj) {
// @ts-expect-error - we are using Ember's Tag not Glimmer's
consumeTag(obj.tag);
} else {
obj.ref;
}
});
}
async function untrack() {
Expand All @@ -125,14 +146,14 @@ async function untrack() {
transaction.cbs.forEach((cb) => {
cb();
});
transaction.props.forEach((obj: Tag) => {
transaction.props.forEach((obj) => {
// mark this mutation as part of a transaction
obj.t = true;
updateRef(obj);
});
}

export function addToTransaction(obj: Tag): void {
export function addToTransaction(obj: Tag | Signal): void {
if (TRANSACTION) {
TRANSACTION.props.add(obj);
} else {
Expand Down Expand Up @@ -213,3 +234,41 @@ export function memoTransact<T extends OpaqueFn>(method: T): (...args: unknown[]
return ret as ReturnType<T>;
};
}

interface Signal {
_debug_base?: string;
_debug_prop?: string;

t: boolean;
shouldReset: boolean;
tag: ReturnType<typeof tagForProperty>;
}

export function createSignal<T extends object, K extends keyof T & string>(obj: T, key: K): Signal {
const _signal: Signal = {
tag: tagForProperty(obj, key),
t: false,
shouldReset: false,
};

if (DEBUG) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-base-to-string
_signal._debug_base = obj.constructor?.name ?? obj.toString?.() ?? 'unknown';
_signal._debug_prop = key;
}

return _signal;
}

export function entangleSignal<T extends object, K extends keyof T & string>(
signals: Map<K, Signal>,
obj: T,
key: K
): void {
let signal = signals.get(key);
if (!signal) {
signal = createSignal(obj, key);
signals.set(key, signal);
}
subscribe(signal);
}
2 changes: 1 addition & 1 deletion packages/tracking/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { transact, memoTransact, untracked } from './-private';
export { transact, memoTransact, untracked, entangleSignal } from './-private';
4 changes: 4 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

86 changes: 86 additions & 0 deletions tests/schema-record/tests/-utils/reactive-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { render, TestContext } from '@ember/test-helpers';
import Component from '@glimmer/component';

import type { FieldSchema } from '@warp-drive/schema-record/schema';

import { hbs } from 'ember-cli-htmlbars';

import type { ResourceRelationship } from '@ember-data/types/cache/relationship';

export async function reactiveContext<T extends object>(this: TestContext, record: T, fields: FieldSchema[]) {
const _fields: string[] = ['idCount', 'id', '$typeCount', '$type'];
fields.forEach((field) => {
_fields.push(field.name + 'Count');
_fields.push(field.name);
});

class ReactiveComponent extends Component {
get __allFields() {
return _fields;
}
}

const counters: Record<string, number> = {};
counters['id'] = 0;
counters['$type'] = 0;

Object.defineProperty(ReactiveComponent.prototype, 'idCount', {
get() {
return counters['id'];
},
});
Object.defineProperty(ReactiveComponent.prototype, '$typeCount', {
get() {
return counters['$type'];
},
});

Object.defineProperty(ReactiveComponent.prototype, 'id', {
get() {
counters['id']++;
return record['id'] as unknown;
},
});
Object.defineProperty(ReactiveComponent.prototype, '$type', {
get() {
counters['$type']++;
return record['$type'] as unknown;
},
});

fields.forEach((field) => {
counters[field.name] = 0;
Object.defineProperty(ReactiveComponent.prototype, field.name + 'Count', {
get() {
return counters[field.name];
},
});
Object.defineProperty(ReactiveComponent.prototype, field.name, {
get() {
counters[field.name]++;

if (field.kind === 'attribute' || field.kind === 'derived') {
return record[field.name] as unknown;
} else if (field.kind === 'resource') {
return (record[field.name] as ResourceRelationship).data?.id;
}
},
});
});

this.owner.register('component:reactive-component', ReactiveComponent);
this.owner.register(
'template:components/reactive-component',
hbs`<div class="reactive-context"><ul>{{#each this.__allFields as |prop|}}<li>{{prop}}: {{get this prop}}</li>{{/each}}</ul></div>`
);

await render(hbs`<ReactiveComponent />`);

function reset() {
fields.forEach((field) => {
counters[field.name] = 0;
});
}

return { counters, reset, fieldOrder: _fields };
}
Loading
Loading