Skip to content

Commit

Permalink
Breaking circular deps with lazy injected values
Browse files Browse the repository at this point in the history
  • Loading branch information
kraenhansen committed Jul 25, 2024
1 parent d358d5d commit fce8d21
Show file tree
Hide file tree
Showing 35 changed files with 609 additions and 415 deletions.
7 changes: 4 additions & 3 deletions packages/realm/src/ClassHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
import type { binding } from "../binding";
import type { CanonicalObjectSchema, DefaultObject, RealmObjectConstructor } from "./schema";
import type { PropertyMap } from "./PropertyMap";
import { INTERNAL_HELPERS, type RealmObject } from "./Object";
import type { RealmObject } from "./Object";
import { OBJECT_HELPERS } from "./symbols";

type ObjectWrapper = (obj: binding.Obj) => (RealmObject & DefaultObject) | null;

Expand All @@ -35,7 +36,7 @@ export type ClassHelpers = {
/** @internal */
export function setClassHelpers(constructor: RealmObjectConstructor, value: ClassHelpers): void {
// Store the properties map on the object class
Object.defineProperty(constructor, INTERNAL_HELPERS, {
Object.defineProperty(constructor, OBJECT_HELPERS, {
enumerable: false,
writable: false,
configurable: false,
Expand All @@ -51,7 +52,7 @@ export function setClassHelpers(constructor: RealmObjectConstructor, value: Clas
* @internal
*/
export function getClassHelpers(arg: typeof RealmObject): ClassHelpers {
const helpers = arg[INTERNAL_HELPERS];
const helpers = arg[OBJECT_HELPERS];
if (helpers) {
return helpers as ClassHelpers;
} else {
Expand Down
6 changes: 3 additions & 3 deletions packages/realm/src/ClassMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
import type { CanonicalObjectSchema, Constructor, RealmObjectConstructor } from "./schema";
import type { binding } from "../binding";
import { PropertyMap } from "./PropertyMap";
import { KEY_ARRAY, KEY_SET, REALM, RealmObject } from "./Object";
import { KEY_ARRAY, KEY_SET, RealmObject } from "./Object";
import { assert } from "./assert";
import { getClassHelpers, setClassHelpers } from "./ClassHelpers";
import { OBJECT_INTERNAL } from "./symbols";
import { OBJECT_INTERNAL, OBJECT_REALM } from "./symbols";

/** @internal */
export class ClassMap {
Expand Down Expand Up @@ -80,7 +80,7 @@ export class ClassMap {
});
}

Object.defineProperty(constructor.prototype, REALM, {
Object.defineProperty(constructor.prototype, OBJECT_REALM, {
enumerable: false,
configurable: false,
writable: false,
Expand Down
6 changes: 5 additions & 1 deletion packages/realm/src/Collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
////////////////////////////////////////////////////////////////////////////

import type { binding } from "../binding";
import type { Dictionary, DictionaryAccessor } from "./Dictionary";
import { injectLazy } from "./lazy";
import type { Dictionary } from "./Dictionary";
import type { List } from "./List";
import type { OrderedCollectionAccessor } from "./OrderedCollection";
import type { RealmSet } from "./Set";
Expand All @@ -26,6 +27,7 @@ import type { TypeHelpers } from "./TypeHelpers";
import { type CallbackAdder, Listeners } from "./Listeners";
import { IllegalConstructorError, type TypeAssertionError } from "./errors";
import { assert } from "./assert";
import type { DictionaryAccessor } from "./collection-accessors/Dictionary";

/**
* Collection accessor identifier.
Expand Down Expand Up @@ -201,3 +203,5 @@ export abstract class Collection<

/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- We define these once to avoid using "any" through the code */
export type AnyCollection = Collection<any, any, any, any, any>;

injectLazy("Collection", Collection);
132 changes: 7 additions & 125 deletions packages/realm/src/Dictionary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@

import { assert } from "./assert";
import { binding } from "../binding";
import { List, createListAccessor, insertIntoListOfMixed, isJsOrRealmList } from "./List";
import { Results, createResultsAccessor } from "./Results";
import { injectLazy, lazy } from "./lazy";
import { COLLECTION_ACCESSOR as ACCESSOR, Collection, COLLECTION_TYPE_HELPERS as TYPE_HELPERS } from "./Collection";
import { AssertionError, IllegalConstructorError } from "./errors";
import type { DefaultObject } from "./schema";
Expand All @@ -28,6 +27,8 @@ import type { Realm } from "./Realm";
import { toItemType } from "./TypeHelpers";
import type { TypeHelpers } from "./TypeHelpers";
import { RealmObject } from "./Object";
import type { DictionaryAccessor } from "./collection-accessors/Dictionary";
import { createResultsAccessor } from "./collection-accessors/Results";

/* eslint-disable jsdoc/multiline-blocks -- We need this to have @ts-expect-error located correctly in the .d.ts bundle */

Expand Down Expand Up @@ -215,7 +216,7 @@ export class Dictionary<T = unknown> extends Collection<
const itemType = toItemType(values.type);
const typeHelpers = this[TYPE_HELPERS];
const accessor = createResultsAccessor({ realm, typeHelpers, itemType });
const results = new Results<T>(realm, values, accessor, typeHelpers);
const results = new lazy.Results<T>(realm, values, accessor, typeHelpers);

for (const value of results.values()) {
yield value;
Expand All @@ -237,7 +238,7 @@ export class Dictionary<T = unknown> extends Collection<
const itemType = toItemType(snapshot.type);
const typeHelpers = this[TYPE_HELPERS];
const accessor = createResultsAccessor({ realm, typeHelpers, itemType });
const results = new Results<T>(realm, snapshot, accessor, typeHelpers);
const results = new lazy.Results<T>(realm, snapshot, accessor, typeHelpers);

for (let i = 0; i < size; i++) {
const key = keys.getAny(i);
Expand Down Expand Up @@ -324,126 +325,7 @@ export class Dictionary<T = unknown> extends Collection<
}
}

/**
* Accessor for getting and setting items in the binding collection.
* @internal
*/
export type DictionaryAccessor<T = unknown> = {
get: (dictionary: binding.Dictionary, key: string) => T;
set: (dictionary: binding.Dictionary, key: string, value: T) => void;
};

type DictionaryAccessorFactoryOptions<T> = {
realm: Realm;
typeHelpers: TypeHelpers<T>;
itemType: binding.PropertyType;
isEmbedded?: boolean;
};

/** @internal */
export function createDictionaryAccessor<T>(options: DictionaryAccessorFactoryOptions<T>): DictionaryAccessor<T> {
return options.itemType === binding.PropertyType.Mixed
? createDictionaryAccessorForMixed<T>(options)
: createDictionaryAccessorForKnownType<T>(options);
}

function createDictionaryAccessorForMixed<T>({
realm,
typeHelpers,
}: Pick<DictionaryAccessorFactoryOptions<T>, "realm" | "typeHelpers">): DictionaryAccessor<T> {
const { toBinding, fromBinding } = typeHelpers;
return {
get(dictionary, key) {
const value = dictionary.tryGetAny(key);
switch (value) {
case binding.ListSentinel: {
const accessor = createListAccessor<T>({ realm, itemType: binding.PropertyType.Mixed, typeHelpers });
return new List<T>(realm, dictionary.getList(key), accessor, typeHelpers) as T;
}
case binding.DictionarySentinel: {
const accessor = createDictionaryAccessor<T>({ realm, itemType: binding.PropertyType.Mixed, typeHelpers });
return new Dictionary<T>(realm, dictionary.getDictionary(key), accessor, typeHelpers) as T;
}
default:
return fromBinding(value) as T;
}
},
set(dictionary, key, value) {
assert.inTransaction(realm);

if (isJsOrRealmList(value)) {
dictionary.insertCollection(key, binding.CollectionType.List);
insertIntoListOfMixed(value, dictionary.getList(key), toBinding);
} else if (isJsOrRealmDictionary(value)) {
dictionary.insertCollection(key, binding.CollectionType.Dictionary);
insertIntoDictionaryOfMixed(value, dictionary.getDictionary(key), toBinding);
} else {
dictionary.insertAny(key, toBinding(value));
}
},
};
}

function createDictionaryAccessorForKnownType<T>({
realm,
typeHelpers,
isEmbedded,
}: Omit<DictionaryAccessorFactoryOptions<T>, "itemType">): DictionaryAccessor<T> {
const { fromBinding, toBinding } = typeHelpers;
return {
get(dictionary, key) {
return fromBinding(dictionary.tryGetAny(key));
},
set(dictionary, key, value) {
assert.inTransaction(realm);

if (isEmbedded) {
toBinding(value, { createObj: () => [dictionary.insertEmbedded(key), true] });
} else {
dictionary.insertAny(key, toBinding(value));
}
},
};
}

/** @internal */
export function insertIntoDictionaryOfMixed(
dictionary: Dictionary | Record<string, unknown>,
internal: binding.Dictionary,
toBinding: TypeHelpers["toBinding"],
) {
// TODO: Solve the "removeAll()" case for self-assignment (https://github.com/realm/realm-core/issues/7422).
internal.removeAll();

for (const key in dictionary) {
const value = dictionary[key];
if (isJsOrRealmList(value)) {
internal.insertCollection(key, binding.CollectionType.List);
insertIntoListOfMixed(value, internal.getList(key), toBinding);
} else if (isJsOrRealmDictionary(value)) {
internal.insertCollection(key, binding.CollectionType.Dictionary);
insertIntoDictionaryOfMixed(value, internal.getDictionary(key), toBinding);
} else {
internal.insertAny(key, toBinding(value));
}
}
}

/** @internal */
export function isJsOrRealmDictionary(value: unknown): value is Dictionary | Record<string, unknown> {
return isPOJO(value) || value instanceof Dictionary;
}

/** @internal */
export function isPOJO(value: unknown): value is Record<string, unknown> {
return (
typeof value === "object" &&
value !== null &&
// Lastly check for the absence of a prototype as POJOs
// can still be created using `Object.create(null)`.
(value.constructor === Object || !Object.getPrototypeOf(value))
);
}

/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- We define these once to avoid using "any" through the code */
export type AnyDictionary = Dictionary<any>;

injectLazy("Dictionary", Dictionary);
6 changes: 6 additions & 0 deletions packages/realm/src/List.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import { assert } from "./assert";
import { binding } from "../binding";
import { injectLazy } from "./lazy";
import { COLLECTION_ACCESSOR as ACCESSOR } from "./Collection";
import { AssertionError, IllegalConstructorError } from "./errors";
import { OrderedCollection } from "./OrderedCollection";
Expand Down Expand Up @@ -309,3 +310,8 @@ export class List<T = unknown>
this.internal.swap(index1, index2);
}
}

/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- We define these once to avoid using "any" through the code */
export type AnyList = List<any>;

injectLazy("List", List);
32 changes: 16 additions & 16 deletions packages/realm/src/Object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import { binding } from "../binding";
import { assert } from "./assert";
import { AssertionError, TypeAssertionError } from "./errors";
import { injectLazy, lazy } from "./lazy";
import { BSON } from "./bson";
import {
type CanonicalObjectSchema,
Expand All @@ -30,16 +31,15 @@ import {
} from "./schema";
import type { ClassHelpers } from "./ClassHelpers";
import type { Collection } from "./Collection";
import { Dictionary } from "./Dictionary";
import { JSONCacheMap } from "./JSONCacheMap";
import { type ObjectChangeCallback, ObjectListeners } from "./ObjectListeners";
import type { OmittedRealmTypes, Unmanaged } from "./Unmanaged";
import { OrderedCollection } from "./OrderedCollection";
import type { Realm } from "./Realm";
import { Results, createResultsAccessor } from "./Results";
import type { Results } from "./Results";
import type { TypeHelpers } from "./TypeHelpers";
import { flags } from "./flags";
import { OBJECT_INTERNAL } from "./symbols";
import { OBJECT_HELPERS, OBJECT_INTERNAL, OBJECT_REALM } from "./symbols";
import { createResultsAccessor } from "./collection-accessors/Results";

/**
* The update mode to use when creating an object that already exists,
Expand Down Expand Up @@ -80,9 +80,7 @@ export type AnyRealmObject = RealmObject<any>;

export const KEY_ARRAY = Symbol("Object#keys");
export const KEY_SET = Symbol("Object#keySet");
export const REALM = Symbol("Object#realm");
const INTERNAL_LISTENERS = Symbol("Object#listeners");
export const INTERNAL_HELPERS = Symbol("Object.helpers");
const DEFAULT_PROPERTY_DESCRIPTOR: PropertyDescriptor = { configurable: true, enumerable: true, writable: true };

/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
Expand Down Expand Up @@ -155,7 +153,7 @@ export class RealmObject<T = DefaultObject, RequiredProperties extends keyof Omi
* This property is stored on the per class prototype when transforming the schema.
* @internal
*/
public static [INTERNAL_HELPERS]: ClassHelpers;
public static [OBJECT_HELPERS]: ClassHelpers;

public static allowValuesArrays = false;

Expand Down Expand Up @@ -329,7 +327,7 @@ export class RealmObject<T = DefaultObject, RequiredProperties extends keyof Omi
* Note: this is on the injected prototype from ClassMap.defineProperties().
* @internal
*/
public declare readonly [REALM]: Realm;
public declare readonly [OBJECT_REALM]: Realm;

/**
* The object's representation in the binding.
Expand Down Expand Up @@ -396,7 +394,7 @@ export class RealmObject<T = DefaultObject, RequiredProperties extends keyof Omi
if (typeof value == "function") {
continue;
}
if (value instanceof RealmObject || value instanceof OrderedCollection || value instanceof Dictionary) {
if (value instanceof lazy.Object || value instanceof lazy.OrderedCollection || value instanceof lazy.Dictionary) {
// recursively trigger `toJSON` for Realm instances with the same cache.
result[key] = value.toJSON(key, cache);
} else {
Expand All @@ -420,7 +418,7 @@ export class RealmObject<T = DefaultObject, RequiredProperties extends keyof Omi
* @returns The {@link CanonicalObjectSchema} that describes this object.
*/
objectSchema(): CanonicalObjectSchema<T> {
return this[REALM].getClassHelpers(this).canonicalObjectSchema as CanonicalObjectSchema<T>;
return this[OBJECT_REALM].getClassHelpers(this).canonicalObjectSchema as CanonicalObjectSchema<T>;
}

/**
Expand All @@ -433,7 +431,7 @@ export class RealmObject<T = DefaultObject, RequiredProperties extends keyof Omi
linkingObjects<T = DefaultObject>(objectType: string, propertyName: string): Results<RealmObject<T> & T>;
linkingObjects<T extends AnyRealmObject>(objectType: Constructor<T>, propertyName: string): Results<T>;
linkingObjects<T extends AnyRealmObject>(objectType: string | Constructor<T>, propertyName: string): Results<T> {
const realm = this[REALM];
const realm = this[OBJECT_REALM];
const targetClassHelpers = realm.getClassHelpers(objectType);
const { objectSchema: targetObjectSchema, properties, wrapObject } = targetClassHelpers;
const targetProperty = properties.get(propertyName);
Expand Down Expand Up @@ -461,7 +459,7 @@ export class RealmObject<T = DefaultObject, RequiredProperties extends keyof Omi
const tableView = this[OBJECT_INTERNAL].getBacklinkView(tableRef, targetProperty.columnKey);
const results = binding.Results.fromTableView(realm.internal, tableView);

return new Results<T>(realm, results, accessor, typeHelpers);
return new lazy.Results<T>(realm, results, accessor, typeHelpers);
}

/**
Expand Down Expand Up @@ -514,7 +512,7 @@ export class RealmObject<T = DefaultObject, RequiredProperties extends keyof Omi
addListener(callback: ObjectChangeCallback<T>, keyPaths?: string | string[]): void {
assert.function(callback);
if (!this[INTERNAL_LISTENERS]) {
this[INTERNAL_LISTENERS] = new ObjectListeners<T>(this[REALM].internal, this);
this[INTERNAL_LISTENERS] = new ObjectListeners<T>(this[OBJECT_REALM].internal, this);
}
this[INTERNAL_LISTENERS].addListener(callback, typeof keyPaths === "string" ? [keyPaths] : keyPaths);
}
Expand Down Expand Up @@ -545,7 +543,7 @@ export class RealmObject<T = DefaultObject, RequiredProperties extends keyof Omi
* @returns Underlying type of the property value.
*/
getPropertyType(propertyName: string): string {
const { properties } = this[REALM].getClassHelpers(this);
const { properties } = this[OBJECT_REALM].getClassHelpers(this);
const { type, objectType, columnKey } = properties.get(propertyName);
const typeName = getTypeName(type, objectType);
if (typeName === "mixed") {
Expand All @@ -560,10 +558,10 @@ export class RealmObject<T = DefaultObject, RequiredProperties extends keyof Omi
} else if (value instanceof binding.Timestamp) {
return "date";
} else if (value instanceof binding.Obj) {
const { objectSchema } = this[REALM].getClassHelpers(value.table.key);
const { objectSchema } = this[OBJECT_REALM].getClassHelpers(value.table.key);
return `<${objectSchema.name}>`;
} else if (value instanceof binding.ObjLink) {
const { objectSchema } = this[REALM].getClassHelpers(value.tableKey);
const { objectSchema } = this[OBJECT_REALM].getClassHelpers(value.tableKey);
return `<${objectSchema.name}>`;
} else if (value instanceof ArrayBuffer) {
return "data";
Expand Down Expand Up @@ -597,3 +595,5 @@ export class RealmObject<T = DefaultObject, RequiredProperties extends keyof Omi
// We like to refer to this as "Realm.Object"
// TODO: Determine if we want to revisit this if we're going away from a namespaced API
Object.defineProperty(RealmObject, "name", { value: "Realm.Object" });

injectLazy("Object", RealmObject);
Loading

0 comments on commit fce8d21

Please sign in to comment.