From fce8d21ae7170a872e8731f402228b725b0ee058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 25 Jul 2024 16:59:17 +0200 Subject: [PATCH] Breaking circular deps with lazy injected values --- packages/realm/src/ClassHelpers.ts | 7 +- packages/realm/src/ClassMap.ts | 6 +- packages/realm/src/Collection.ts | 6 +- packages/realm/src/Dictionary.ts | 132 +--------------- packages/realm/src/List.ts | 6 + packages/realm/src/Object.ts | 32 ++-- packages/realm/src/OrderedCollection.ts | 68 +++------ packages/realm/src/ProgressRealmPromise.ts | 13 +- packages/realm/src/Realm.ts | 15 +- packages/realm/src/Results.ts | 61 +------- packages/realm/src/Set.ts | 88 +---------- packages/realm/src/app-services/App.ts | 10 +- .../src/app-services/BaseSubscriptionSet.ts | 7 +- .../app-services/MutableSubscriptionSet.ts | 6 +- .../realm/src/app-services/SyncSession.ts | 14 +- packages/realm/src/app-services/User.ts | 13 +- .../src/collection-accessors/Dictionary.ts | 142 ++++++++++++++++++ .../realm/src/collection-accessors/List.ts | 27 +--- .../collection-accessors/OrderedCollection.ts | 53 +++++++ .../realm/src/collection-accessors/Results.ts | 73 +++++++++ .../realm/src/collection-accessors/Set.ts | 101 +++++++++++++ packages/realm/src/lazy.ts | 75 +++++++++ packages/realm/src/platform/node/index.ts | 4 +- .../realm/src/platform/react-native/index.ts | 2 +- .../realm/src/property-accessors/Array.ts | 6 +- .../src/property-accessors/Dictionary.ts | 3 +- .../realm/src/property-accessors/Mixed.ts | 7 +- packages/realm/src/property-accessors/Set.ts | 3 +- .../realm/src/property-accessors/types.ts | 2 +- packages/realm/src/schema/validate.ts | 4 +- packages/realm/src/symbols.ts | 2 + .../src/tests/collection-helpers.test.ts | 2 +- packages/realm/src/type-helpers/Array.ts | 4 +- packages/realm/src/type-helpers/Mixed.ts | 24 ++- packages/realm/src/type-helpers/Object.ts | 6 +- 35 files changed, 609 insertions(+), 415 deletions(-) create mode 100644 packages/realm/src/collection-accessors/Dictionary.ts create mode 100644 packages/realm/src/collection-accessors/OrderedCollection.ts create mode 100644 packages/realm/src/collection-accessors/Results.ts create mode 100644 packages/realm/src/collection-accessors/Set.ts create mode 100644 packages/realm/src/lazy.ts diff --git a/packages/realm/src/ClassHelpers.ts b/packages/realm/src/ClassHelpers.ts index b7f341a4eff..6bc8dc6af15 100644 --- a/packages/realm/src/ClassHelpers.ts +++ b/packages/realm/src/ClassHelpers.ts @@ -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; @@ -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, @@ -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 { diff --git a/packages/realm/src/ClassMap.ts b/packages/realm/src/ClassMap.ts index ed254ca4167..95679c2c66b 100644 --- a/packages/realm/src/ClassMap.ts +++ b/packages/realm/src/ClassMap.ts @@ -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 { @@ -80,7 +80,7 @@ export class ClassMap { }); } - Object.defineProperty(constructor.prototype, REALM, { + Object.defineProperty(constructor.prototype, OBJECT_REALM, { enumerable: false, configurable: false, writable: false, diff --git a/packages/realm/src/Collection.ts b/packages/realm/src/Collection.ts index e984b10549c..ab430d7f2be 100644 --- a/packages/realm/src/Collection.ts +++ b/packages/realm/src/Collection.ts @@ -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"; @@ -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. @@ -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; + +injectLazy("Collection", Collection); diff --git a/packages/realm/src/Dictionary.ts b/packages/realm/src/Dictionary.ts index 325ce3111a8..7eea5a8fd49 100644 --- a/packages/realm/src/Dictionary.ts +++ b/packages/realm/src/Dictionary.ts @@ -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"; @@ -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 */ @@ -215,7 +216,7 @@ export class Dictionary extends Collection< const itemType = toItemType(values.type); const typeHelpers = this[TYPE_HELPERS]; const accessor = createResultsAccessor({ realm, typeHelpers, itemType }); - const results = new Results(realm, values, accessor, typeHelpers); + const results = new lazy.Results(realm, values, accessor, typeHelpers); for (const value of results.values()) { yield value; @@ -237,7 +238,7 @@ export class Dictionary extends Collection< const itemType = toItemType(snapshot.type); const typeHelpers = this[TYPE_HELPERS]; const accessor = createResultsAccessor({ realm, typeHelpers, itemType }); - const results = new Results(realm, snapshot, accessor, typeHelpers); + const results = new lazy.Results(realm, snapshot, accessor, typeHelpers); for (let i = 0; i < size; i++) { const key = keys.getAny(i); @@ -324,126 +325,7 @@ export class Dictionary extends Collection< } } -/** - * Accessor for getting and setting items in the binding collection. - * @internal - */ -export type DictionaryAccessor = { - get: (dictionary: binding.Dictionary, key: string) => T; - set: (dictionary: binding.Dictionary, key: string, value: T) => void; -}; - -type DictionaryAccessorFactoryOptions = { - realm: Realm; - typeHelpers: TypeHelpers; - itemType: binding.PropertyType; - isEmbedded?: boolean; -}; - -/** @internal */ -export function createDictionaryAccessor(options: DictionaryAccessorFactoryOptions): DictionaryAccessor { - return options.itemType === binding.PropertyType.Mixed - ? createDictionaryAccessorForMixed(options) - : createDictionaryAccessorForKnownType(options); -} - -function createDictionaryAccessorForMixed({ - realm, - typeHelpers, -}: Pick, "realm" | "typeHelpers">): DictionaryAccessor { - const { toBinding, fromBinding } = typeHelpers; - return { - get(dictionary, key) { - const value = dictionary.tryGetAny(key); - switch (value) { - case binding.ListSentinel: { - const accessor = createListAccessor({ realm, itemType: binding.PropertyType.Mixed, typeHelpers }); - return new List(realm, dictionary.getList(key), accessor, typeHelpers) as T; - } - case binding.DictionarySentinel: { - const accessor = createDictionaryAccessor({ realm, itemType: binding.PropertyType.Mixed, typeHelpers }); - return new Dictionary(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({ - realm, - typeHelpers, - isEmbedded, -}: Omit, "itemType">): DictionaryAccessor { - 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, - 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 { - return isPOJO(value) || value instanceof Dictionary; -} - -/** @internal */ -export function isPOJO(value: unknown): value is Record { - 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; + +injectLazy("Dictionary", Dictionary); diff --git a/packages/realm/src/List.ts b/packages/realm/src/List.ts index 9af2bd1a13e..8a6d07863cc 100644 --- a/packages/realm/src/List.ts +++ b/packages/realm/src/List.ts @@ -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"; @@ -309,3 +310,8 @@ export class List 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; + +injectLazy("List", List); diff --git a/packages/realm/src/Object.ts b/packages/realm/src/Object.ts index 147b735c062..83323296393 100644 --- a/packages/realm/src/Object.ts +++ b/packages/realm/src/Object.ts @@ -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, @@ -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, @@ -80,9 +80,7 @@ export type AnyRealmObject = RealmObject; 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 */ @@ -155,7 +153,7 @@ export class RealmObject { - return this[REALM].getClassHelpers(this).canonicalObjectSchema as CanonicalObjectSchema; + return this[OBJECT_REALM].getClassHelpers(this).canonicalObjectSchema as CanonicalObjectSchema; } /** @@ -433,7 +431,7 @@ export class RealmObject(objectType: string, propertyName: string): Results & T>; linkingObjects(objectType: Constructor, propertyName: string): Results; linkingObjects(objectType: string | Constructor, propertyName: string): Results { - 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); @@ -461,7 +459,7 @@ export class RealmObject(realm, results, accessor, typeHelpers); + return new lazy.Results(realm, results, accessor, typeHelpers); } /** @@ -514,7 +512,7 @@ export class RealmObject, keyPaths?: string | string[]): void { assert.function(callback); if (!this[INTERNAL_LISTENERS]) { - this[INTERNAL_LISTENERS] = new ObjectListeners(this[REALM].internal, this); + this[INTERNAL_LISTENERS] = new ObjectListeners(this[OBJECT_REALM].internal, this); } this[INTERNAL_LISTENERS].addListener(callback, typeof keyPaths === "string" ? [keyPaths] : keyPaths); } @@ -545,7 +543,7 @@ export class RealmObject`; } 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"; @@ -597,3 +595,5 @@ export class RealmObject = { } catch (err) { // Let the custom errors from Results take precedence over out of bounds errors. This will // let users know that they cannot modify Results, rather than erroring on incorrect index. - if (index < 0 && !(target instanceof Results)) { + if (index < 0 && !(target instanceof lazy.Results)) { throw new Error(`Cannot set item at negative index ${index}.`); } throw err; @@ -397,13 +401,13 @@ export abstract class OrderedCollection< if (this.type === "object") { assert.instanceOf(searchElement, RealmObject); return this.results.indexOfObj(searchElement[OBJECT_INTERNAL]); - } else if (isJsOrRealmList(searchElement) || isJsOrRealmDictionary(searchElement)) { - // Collections are always treated as not equal since their - // references will always be different for each access. - const NOT_FOUND = -1; - return NOT_FOUND; } else { - return this.results.indexOf(this[TYPE_HELPERS].toBinding(searchElement)); + try { + return this.results.indexOf(this[TYPE_HELPERS].toBinding(searchElement)); + } catch { + // Inability to convert to the binding representation means we won't be able to find it. + return INDEX_NOT_FOUND; + } } } /** @@ -824,7 +828,7 @@ export abstract class OrderedCollection< const itemType = toItemType(results.type); const typeHelpers = this[TYPE_HELPERS]; const accessor = createResultsAccessor({ realm, typeHelpers, itemType }); - return new Results(realm, results, accessor, typeHelpers); + return new lazy.Results(realm, results, accessor, typeHelpers); } /** @internal */ @@ -914,7 +918,7 @@ export abstract class OrderedCollection< const itemType = toItemType(results.type); const typeHelpers = this[TYPE_HELPERS]; const accessor = createResultsAccessor({ realm, typeHelpers, itemType }); - return new Results(realm, results, accessor, typeHelpers); + return new lazy.Results(realm, results, accessor, typeHelpers); } else if (typeof arg0 === "string") { return this.sorted([[arg0, arg1 === true]]); } else if (typeof arg0 === "boolean") { @@ -944,7 +948,7 @@ export abstract class OrderedCollection< const itemType = toItemType(snapshot.type); const typeHelpers = this[TYPE_HELPERS]; const accessor = createResultsAccessor({ realm, typeHelpers, itemType }); - return new Results(realm, snapshot, accessor, typeHelpers); + return new lazy.Results(realm, snapshot, accessor, typeHelpers); } /** @internal */ @@ -965,34 +969,4 @@ export abstract class OrderedCollection< } } -type Getter = (collection: CollectionType, index: number) => T; - -type GetterFactoryOptions = { - fromBinding: TypeHelpers["fromBinding"]; - itemType: binding.PropertyType; -}; - -/** @internal */ -export function createDefaultGetter({ - fromBinding, - itemType, -}: GetterFactoryOptions): Getter { - const isObjectItem = itemType === binding.PropertyType.Object || itemType === binding.PropertyType.LinkingObjects; - return isObjectItem ? (...args) => getObject(fromBinding, ...args) : (...args) => getKnownType(fromBinding, ...args); -} - -function getObject( - fromBinding: TypeHelpers["fromBinding"], - collection: OrderedCollectionInternal, - index: number, -): T { - return fromBinding(collection.getObj(index)); -} - -function getKnownType( - fromBinding: TypeHelpers["fromBinding"], - collection: OrderedCollectionInternal, - index: number, -): T { - return fromBinding(collection.getAny(index)); -} +injectLazy("OrderedCollection", OrderedCollection); diff --git a/packages/realm/src/ProgressRealmPromise.ts b/packages/realm/src/ProgressRealmPromise.ts index a179cbaeff1..394f964421f 100644 --- a/packages/realm/src/ProgressRealmPromise.ts +++ b/packages/realm/src/ProgressRealmPromise.ts @@ -20,13 +20,14 @@ import { binding } from "../binding"; import { assert } from "./assert"; import { TimeoutError } from "./errors"; import { flags } from "./flags"; +import { lazy } from "./lazy"; import { type Configuration, validateConfiguration } from "./Configuration"; import { OpenRealmBehaviorType, OpenRealmTimeOutBehavior } from "./app-services/SyncConfiguration"; import { SubscriptionSetState } from "./app-services/BaseSubscriptionSet"; import { type ProgressNotificationCallback, isEstimateProgressNotificationCallback } from "./app-services/SyncSession"; import { PromiseHandle } from "./PromiseHandle"; import { TimeoutPromise } from "./TimeoutPromise"; -import { Realm } from "./Realm"; +import type { Realm } from "./Realm"; type OpenBehavior = { openBehavior: OpenRealmBehaviorType; @@ -94,13 +95,13 @@ export class ProgressRealmPromise implements Promise { // Calling `Realm.exists()` before `binding.Realm.getSynchronizedRealm()` is necessary to capture // the correct value when this constructor was called since `binding.Realm.getSynchronizedRealm()` // will open the realm. This is needed when calling the Realm constructor. - const realmExists = Realm.exists(config); + const realmExists = lazy.Realm.exists(config); const { openBehavior, timeOut, timeOutBehavior } = determineBehavior(config, realmExists); if (openBehavior === OpenRealmBehaviorType.OpenImmediately) { - const realm = new Realm(config); + const realm = new lazy.Realm(config); this.handle.resolve(realm); } else if (openBehavior === OpenRealmBehaviorType.DownloadBeforeOpen) { - const { bindingConfig } = Realm.transformConfig(config); + const { bindingConfig } = lazy.Realm.transformConfig(config); // Construct an async open task this.task = binding.Realm.getSynchronizedRealm(bindingConfig); @@ -113,7 +114,7 @@ export class ProgressRealmPromise implements Promise { this.task .start() .then(async (tsr) => { - const realm = new Realm(config, { + const realm = new lazy.Realm(config, { internal: binding.Helpers.consumeThreadSafeReferenceToSharedRealm(tsr), // Do not call `Realm.exists()` here in case the realm has been opened by this point in time. realmExists, @@ -217,7 +218,7 @@ export class ProgressRealmPromise implements Promise { this.timeoutPromise.catch((err) => { if (err instanceof TimeoutError) { this.cancelAndResetTask(); - const realm = new Realm(config); + const realm = new lazy.Realm(config); this.handle.resolve(realm); } else { this.handle.reject(err); diff --git a/packages/realm/src/Realm.ts b/packages/realm/src/Realm.ts index 6a3efe57917..244aabf5552 100644 --- a/packages/realm/src/Realm.ts +++ b/packages/realm/src/Realm.ts @@ -21,10 +21,11 @@ import { assert } from "./assert"; import { TypeAssertionError } from "./errors"; import { extendDebug } from "./debug"; import { flags } from "./flags"; +import { injectLazy } from "./lazy"; import { fs, garbageCollection } from "./platform"; import type { Unmanaged } from "./Unmanaged"; import { type AnyRealmObject, RealmObject } from "./Object"; -import { type AnyResults, Results, createResultsAccessor } from "./Results"; +import { type AnyResults, Results } from "./Results"; import { type CanonicalObjectSchema, type Constructor, @@ -57,13 +58,14 @@ import { } from "./Logger"; import { type AnyList, List } from "./List"; import { ProgressRealmPromise } from "./ProgressRealmPromise"; -import { REALM, UpdateMode } from "./Object"; +import { UpdateMode } from "./Object"; import { RealmEvent, type RealmListenerCallback, RealmListeners } from "./RealmListeners"; import { SubscriptionSet } from "./app-services/SubscriptionSet"; import { SyncSession } from "./app-services/SyncSession"; import type { TypeHelpers } from "./TypeHelpers"; import { toArrayBuffer } from "./type-helpers/array-buffer"; -import { OBJECT_INTERNAL } from "./symbols"; +import { OBJECT_INTERNAL, OBJECT_REALM } from "./symbols"; +import { createResultsAccessor } from "./collection-accessors/Results"; const debug = extendDebug("Realm"); @@ -801,7 +803,7 @@ export class Realm { assert.inTransaction(this, "Can only delete objects within a transaction."); assert.object(subject, "subject"); if (subject instanceof RealmObject) { - assert.isSameRealm(subject[REALM].internal, this.internal, "Can't delete an object from another Realm"); + assert.isSameRealm(subject[OBJECT_REALM].internal, this.internal, "Can't delete an object from another Realm"); const { objectSchema } = this.classes.getHelpers(subject); const obj = subject[OBJECT_INTERNAL]; assert.isValid( @@ -818,7 +820,7 @@ export class Realm { //@ts-expect-error the above check is good enough for (const object of subject) { assert.instanceOf(object, RealmObject); - assert.isSameRealm(object[REALM].internal, this.internal, "Can't delete an object from another Realm"); + assert.isSameRealm(object[OBJECT_REALM].internal, this.internal, "Can't delete an object from another Realm"); const { objectSchema } = this.classes.getHelpers(object); const table = binding.Helpers.getTable(this.internal, objectSchema.tableKey); table.removeObject(object[OBJECT_INTERNAL].key); @@ -1185,6 +1187,8 @@ export class Realm { } } +injectLazy("Realm", Realm); + /** * @param objectSchema - The schema of the object. * @returns `true` if the object is marked for asymmetric sync, otherwise `false`. @@ -1206,6 +1210,7 @@ function isEmbedded(objectSchema: binding.ObjectSchema): boolean { // @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-namespaces-with-classes-functions-and-enums import * as ns from "./namespace"; + // Needed to avoid complaints about a self-reference import RealmItself = Realm; diff --git a/packages/realm/src/Results.ts b/packages/realm/src/Results.ts index 2de7e930a6a..607a6becc6b 100644 --- a/packages/realm/src/Results.ts +++ b/packages/realm/src/Results.ts @@ -19,15 +19,15 @@ import { binding } from "../binding"; import { assert } from "./assert"; import { IllegalConstructorError } from "./errors"; +import { injectLazy } from "./lazy"; import { COLLECTION_ACCESSOR as ACCESSOR } from "./Collection"; -import { Dictionary, createDictionaryAccessor } from "./Dictionary"; -import { List, createListAccessor } from "./List"; -import { OrderedCollection, createDefaultGetter } from "./OrderedCollection"; +import { OrderedCollection } from "./OrderedCollection"; import type { Realm } from "./Realm"; import { type SubscriptionOptions, WaitForSync } from "./app-services/MutableSubscriptionSet"; import { TimeoutPromise } from "./TimeoutPromise"; import type { TypeHelpers } from "./TypeHelpers"; import type { Unmanaged } from "./Unmanaged"; +import type { ResultsAccessor } from "./collection-accessors/Results"; /** * Instances of this class are typically **live** collections returned by @@ -190,58 +190,7 @@ export class Results extends OrderedCollection< } } -/** - * Accessor for getting items from the binding collection. - * @internal - */ -export type ResultsAccessor = { - get: (results: binding.Results, index: number) => T; -}; - -type ResultsAccessorFactoryOptions = { - realm: Realm; - typeHelpers: TypeHelpers; - itemType: binding.PropertyType; -}; - -/** @internal */ -export function createResultsAccessor(options: ResultsAccessorFactoryOptions): ResultsAccessor { - return options.itemType === binding.PropertyType.Mixed - ? createResultsAccessorForMixed(options) - : createResultsAccessorForKnownType(options); -} - -function createResultsAccessorForMixed({ - realm, - typeHelpers, -}: Omit, "itemType">): ResultsAccessor { - return { - get(results, index) { - const value = results.getAny(index); - switch (value) { - case binding.ListSentinel: { - const accessor = createListAccessor({ realm, typeHelpers, itemType: binding.PropertyType.Mixed }); - return new List(realm, results.getList(index), accessor, typeHelpers) as T; - } - case binding.DictionarySentinel: { - const accessor = createDictionaryAccessor({ realm, typeHelpers, itemType: binding.PropertyType.Mixed }); - return new Dictionary(realm, results.getDictionary(index), accessor, typeHelpers) as T; - } - default: - return typeHelpers.fromBinding(value); - } - }, - }; -} - -function createResultsAccessorForKnownType({ - typeHelpers, - itemType, -}: Omit, "realm">): ResultsAccessor { - return { - get: createDefaultGetter({ fromBinding: typeHelpers.fromBinding, itemType }), - }; -} - /* eslint-disable-next-line @typescript-eslint/no-explicit-any -- Useful for APIs taking any `Results` */ export type AnyResults = Results; + +injectLazy("Results", Results); diff --git a/packages/realm/src/Set.ts b/packages/realm/src/Set.ts index 8d29b6bb5f2..d59277bb1ac 100644 --- a/packages/realm/src/Set.ts +++ b/packages/realm/src/Set.ts @@ -19,10 +19,12 @@ import { binding } from "../binding"; import { assert } from "./assert"; import { IllegalConstructorError } from "./errors"; +import { injectLazy } from "./lazy"; import { COLLECTION_ACCESSOR as ACCESSOR, COLLECTION_TYPE_HELPERS as TYPE_HELPERS } from "./Collection"; -import { OrderedCollection, createDefaultGetter } from "./OrderedCollection"; +import { OrderedCollection } from "./OrderedCollection"; import type { Realm } from "./Realm"; import type { TypeHelpers } from "./TypeHelpers"; +import type { SetAccessor } from "./collection-accessors/Set"; /** * Instances of this class will be returned when accessing object properties whose type is `"Set"` @@ -145,87 +147,7 @@ export class RealmSet extends OrderedCollection< } } -/** - * Accessor for getting and setting items in the binding collection. - * @internal - */ -export type SetAccessor = { - get: (set: binding.Set, index: number) => T; - set: (set: binding.Set, index: number, value: T) => void; - insert: (set: binding.Set, value: T) => void; -}; - -type SetAccessorFactoryOptions = { - realm: Realm; - typeHelpers: TypeHelpers; - itemType: binding.PropertyType; -}; - -/** @internal */ -export function createSetAccessor(options: SetAccessorFactoryOptions): SetAccessor { - return options.itemType === binding.PropertyType.Mixed - ? createSetAccessorForMixed(options) - : createSetAccessorForKnownType(options); -} - -function createSetAccessorForMixed({ - realm, - typeHelpers, -}: Omit, "itemType">): SetAccessor { - const { fromBinding, toBinding } = typeHelpers; - return { - get(set, index) { - // Core will not return collections within a Set. - return fromBinding(set.getAny(index)); - }, - // Directly setting by "index" to a Set is a no-op. - set: () => {}, - insert(set, value) { - assert.inTransaction(realm); - - try { - set.insertAny(toBinding(value)); - } catch (err) { - // Optimize for the valid cases by not guarding for the unsupported nested collections upfront. - throw transformError(err); - } - }, - }; -} - -function createSetAccessorForKnownType({ - realm, - typeHelpers, - itemType, -}: SetAccessorFactoryOptions): SetAccessor { - const { fromBinding, toBinding } = typeHelpers; - return { - get: createDefaultGetter({ fromBinding, itemType }), - // Directly setting by "index" to a Set is a no-op. - set: () => {}, - insert(set, value) { - assert.inTransaction(realm); - - try { - set.insertAny(toBinding(value)); - } catch (err) { - // Optimize for the valid cases by not guarding for the unsupported nested collections upfront. - throw transformError(err); - } - }, - }; -} - -function transformError(err: unknown) { - const message = err instanceof Error ? err.message : ""; - if (message?.includes("'Array' to a Mixed") || message?.includes("'List' to a Mixed")) { - return new Error("Lists within a Set are not supported."); - } - if (message?.includes("'Object' to a Mixed") || message?.includes("'Dictionary' to a Mixed")) { - return new Error("Dictionaries within a Set are not supported."); - } - return err; -} - /* eslint-disable-next-line @typescript-eslint/no-explicit-any -- We define these once to avoid using "any" through the code */ export type AnySet = RealmSet; + +injectLazy("Set", RealmSet); diff --git a/packages/realm/src/app-services/App.ts b/packages/realm/src/app-services/App.ts index d9415400798..46e3a272b1d 100644 --- a/packages/realm/src/app-services/App.ts +++ b/packages/realm/src/app-services/App.ts @@ -20,6 +20,7 @@ import type { AnyFetch } from "@realm/fetch"; import { binding } from "../../binding"; import { assert } from "../assert"; +import { injectLazy } from "../lazy"; import type { BaseConfiguration } from "../Configuration"; import type { DefaultObject } from "../schema"; import { Listeners } from "../Listeners"; @@ -407,7 +408,10 @@ export class App< } } -import * as ns from "../namespace"; +injectLazy("App", App); + +import type * as CredentialsNS from "./Credentials"; +import { Sync as SyncNS } from "./Sync"; // eslint-disable-next-line @typescript-eslint/no-namespace export namespace App { @@ -415,6 +419,6 @@ export namespace App { * All credentials available for authentication. * @see https://www.mongodb.com/docs/atlas/app-services/authentication/ */ - export type Credentials = ns.Credentials; - export import Sync = ns.Sync; + export type Credentials = CredentialsNS.Credentials; + export import Sync = SyncNS; } diff --git a/packages/realm/src/app-services/BaseSubscriptionSet.ts b/packages/realm/src/app-services/BaseSubscriptionSet.ts index a88f10c87d0..269da00cd18 100644 --- a/packages/realm/src/app-services/BaseSubscriptionSet.ts +++ b/packages/realm/src/app-services/BaseSubscriptionSet.ts @@ -18,8 +18,9 @@ import { binding } from "../../binding"; import { assert } from "../assert"; -import { type AnyResults, Results } from "../Results"; -import type { Realm } from "../Realm"; +import { lazy } from "../lazy"; +import type { Results } from "../Results"; +import { type AnyResults } from "../Results"; import type { RealmObject } from "../Object"; import type { MutableSubscriptionSet } from "./MutableSubscriptionSet"; import { Subscription } from "./Subscription"; @@ -215,7 +216,7 @@ export abstract class BaseSubscriptionSet { * @returns The subscription with the specified query, or `null` if the subscription is not found. */ findByQuery(query: Results>): Subscription | null { - assert.instanceOf(query, Results, "query"); + assert.instanceOf(query, lazy.Results, "query"); const subscription = this.internal.findByQuery(query.internal.query); return subscription ? (new Subscription(subscription) as Subscription) : null; // TODO: Remove the type assertion into Subscription diff --git a/packages/realm/src/app-services/MutableSubscriptionSet.ts b/packages/realm/src/app-services/MutableSubscriptionSet.ts index b6781999f4c..09d6db741e5 100644 --- a/packages/realm/src/app-services/MutableSubscriptionSet.ts +++ b/packages/realm/src/app-services/MutableSubscriptionSet.ts @@ -18,9 +18,9 @@ import { binding } from "../../binding"; import { assert } from "../assert"; +import { lazy } from "../lazy"; import type { Realm } from "../Realm"; import type { AnyResults } from "../Results"; -import { Results } from "../Results"; import { BaseSubscriptionSet } from "./BaseSubscriptionSet"; import { Subscription } from "./Subscription"; import type { SubscriptionSet } from "./SubscriptionSet"; @@ -106,7 +106,7 @@ export class MutableSubscriptionSet extends BaseSubscriptionSet { * @returns A `Subscription` instance for the new subscription. */ add(query: AnyResults, options?: SubscriptionOptions): Subscription { - assert.instanceOf(query, Results, "query"); + assert.instanceOf(query, lazy.Results, "query"); if (options) { validateSubscriptionOptions(options); } @@ -145,7 +145,7 @@ export class MutableSubscriptionSet extends BaseSubscriptionSet { * @returns `true` if the subscription was removed, `false` if it was not found. */ remove(query: AnyResults): boolean { - assert.instanceOf(query, Results, "query"); + assert.instanceOf(query, lazy.Results, "query"); return this.internal.eraseByQuery(query.internal.query); } diff --git a/packages/realm/src/app-services/SyncSession.ts b/packages/realm/src/app-services/SyncSession.ts index fef62c75466..297738139de 100644 --- a/packages/realm/src/app-services/SyncSession.ts +++ b/packages/realm/src/app-services/SyncSession.ts @@ -21,8 +21,8 @@ import { EJSON } from "bson"; import { binding } from "../../binding"; import { assert } from "../assert"; import { ClientResetError, fromBindingSyncError } from "../errors"; +import { lazy } from "../lazy"; import { Listeners } from "../Listeners"; -import { Realm } from "../Realm"; import { TimeoutPromise } from "../TimeoutPromise"; import type { App } from "./App"; import { @@ -205,7 +205,7 @@ export function toBindingErrorHandlerWithOnManual( /** @internal */ export function toBindingNotifyBeforeClientReset(onBefore: ClientResetBeforeCallback) { return (internal: binding.Realm) => { - onBefore(new Realm(null, { internal })); + onBefore(new lazy.Realm(null, { internal })); }; } @@ -213,8 +213,8 @@ export function toBindingNotifyBeforeClientReset(onBefore: ClientResetBeforeCall export function toBindingNotifyAfterClientReset(onAfter: ClientResetAfterCallback) { return (internal: binding.Realm, tsr: binding.ThreadSafeReference) => { onAfter( - new Realm(null, { internal }), - new Realm(null, { internal: binding.Helpers.consumeThreadSafeReferenceToSharedRealm(tsr) }), + new lazy.Realm(null, { internal }), + new lazy.Realm(null, { internal: binding.Helpers.consumeThreadSafeReferenceToSharedRealm(tsr) }), ); }; } @@ -227,11 +227,11 @@ export function toBindingNotifyAfterClientResetWithFallback( return (internal: binding.Realm, tsr: binding.ThreadSafeReference, didRecover: boolean) => { if (didRecover) { onAfter( - new Realm(null, { internal }), - new Realm(null, { internal: binding.Helpers.consumeThreadSafeReferenceToSharedRealm(tsr) }), + new lazy.Realm(null, { internal }), + new lazy.Realm(null, { internal: binding.Helpers.consumeThreadSafeReferenceToSharedRealm(tsr) }), ); } else { - const realm = new Realm(null, { internal: binding.Helpers.consumeThreadSafeReferenceToSharedRealm(tsr) }); + const realm = new lazy.Realm(null, { internal: binding.Helpers.consumeThreadSafeReferenceToSharedRealm(tsr) }); if (onFallback) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion onFallback(realm.syncSession!, realm.path); diff --git a/packages/realm/src/app-services/User.ts b/packages/realm/src/app-services/User.ts index 90930b32a6c..ad37426ef19 100644 --- a/packages/realm/src/app-services/User.ts +++ b/packages/realm/src/app-services/User.ts @@ -18,12 +18,14 @@ import { binding } from "../../binding"; import { assert } from "../assert"; +import { injectLazy, lazy } from "../lazy"; import { network } from "../platform"; import type { DefaultObject } from "../schema"; import { Listeners } from "../Listeners"; import { asyncIteratorFromResponse } from "../async-iterator-from-response"; import { cleanArguments } from "./utils"; -import { type AnyApp, App } from "./App"; +import type { App } from "./App"; +import type { AnyApp } from "./App"; import { ApiKeyAuth } from "./ApiKeyAuth"; import type { ProviderType } from "./Credentials"; import { type Credentials, isProviderType } from "./Credentials"; @@ -106,10 +108,13 @@ export class User< >(internal: binding.User, app?: AnyApp) { // Update the static user reference to the current app if (app) { - App.setAppByUser(internal, app); + lazy.App.setAppByUser(internal, app); } // TODO: Use a WeakRef to memoize the SDK object - return new User(internal, App.getAppByUser(internal)); + return new User( + internal, + lazy.App.getAppByUser(internal), + ); } /** @internal */ @@ -407,3 +412,5 @@ export class User< this.listeners.removeAll(); } } + +injectLazy("User", User); diff --git a/packages/realm/src/collection-accessors/Dictionary.ts b/packages/realm/src/collection-accessors/Dictionary.ts new file mode 100644 index 00000000000..7365909d449 --- /dev/null +++ b/packages/realm/src/collection-accessors/Dictionary.ts @@ -0,0 +1,142 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +import { binding } from "../../binding"; +import { assert } from "../assert"; +import { lazy } from "../lazy"; +import type { Dictionary } from "../Dictionary"; +import { createListAccessor, insertIntoListOfMixed, isJsOrRealmList } from "./List"; +import type { TypeHelpers } from "../TypeHelpers"; +import type { Realm } from "../Realm"; + +export type DictionaryAccessor = { + get: (dictionary: binding.Dictionary, key: string) => T; + set: (dictionary: binding.Dictionary, key: string, value: T) => void; +}; + +type DictionaryAccessorFactoryOptions = { + realm: Realm; + typeHelpers: TypeHelpers; + itemType: binding.PropertyType; + isEmbedded?: boolean; +}; + +/** @internal */ +export function createDictionaryAccessor(options: DictionaryAccessorFactoryOptions): DictionaryAccessor { + return options.itemType === binding.PropertyType.Mixed + ? createDictionaryAccessorForMixed(options) + : createDictionaryAccessorForKnownType(options); +} + +function createDictionaryAccessorForMixed({ + realm, + typeHelpers, +}: Pick, "realm" | "typeHelpers">): DictionaryAccessor { + const { toBinding, fromBinding } = typeHelpers; + return { + get(dictionary, key) { + const value = dictionary.tryGetAny(key); + switch (value) { + case binding.ListSentinel: { + const accessor = createListAccessor({ realm, itemType: binding.PropertyType.Mixed, typeHelpers }); + return new lazy.List(realm, dictionary.getList(key), accessor, typeHelpers) as T; + } + case binding.DictionarySentinel: { + const accessor = createDictionaryAccessor({ realm, itemType: binding.PropertyType.Mixed, typeHelpers }); + return new lazy.Dictionary(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({ + realm, + typeHelpers, + isEmbedded, +}: Omit, "itemType">): DictionaryAccessor { + 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, + 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 { + return isPOJO(value) || value instanceof lazy.Dictionary; +} + +/** @internal */ +export function isPOJO(value: unknown): value is Record { + 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)) + ); +} diff --git a/packages/realm/src/collection-accessors/List.ts b/packages/realm/src/collection-accessors/List.ts index 14bbfdc4bc7..325b3dd5354 100644 --- a/packages/realm/src/collection-accessors/List.ts +++ b/packages/realm/src/collection-accessors/List.ts @@ -18,16 +18,12 @@ import { binding } from "../../binding"; import { assert } from "../assert"; -import { Results } from "../Results"; -import { - Dictionary, - createDictionaryAccessor, - insertIntoDictionaryOfMixed, - isJsOrRealmDictionary, -} from "../Dictionary"; -import { createDefaultGetter } from "../OrderedCollection"; +import { lazy } from "../lazy"; +import { createDictionaryAccessor, insertIntoDictionaryOfMixed, isJsOrRealmDictionary } from "./Dictionary"; +import { createDefaultGetter } from "./OrderedCollection"; import type { TypeHelpers } from "../TypeHelpers"; -import { List } from "../List"; +import type { Realm } from "../Realm"; +import type { List } from "../List"; export type ListAccessor = { get: (list: binding.List, index: number) => T; @@ -60,11 +56,11 @@ function createListAccessorForMixed({ switch (value) { case binding.ListSentinel: { const accessor = createListAccessor({ realm, typeHelpers, itemType: binding.PropertyType.Mixed }); - return new List(realm, list.getList(index), accessor, typeHelpers) as T; + return new lazy.List(realm, list.getList(index), accessor, typeHelpers) as T; } case binding.DictionarySentinel: { const accessor = createDictionaryAccessor({ realm, typeHelpers, itemType: binding.PropertyType.Mixed }); - return new Dictionary(realm, list.getDictionary(index), accessor, typeHelpers) as T; + return new lazy.Dictionary(realm, list.getDictionary(index), accessor, typeHelpers) as T; } default: return typeHelpers.fromBinding(value); @@ -151,12 +147,5 @@ export function insertIntoListOfMixed( /** @internal */ export function isJsOrRealmList(value: unknown): value is List | unknown[] { - return Array.isArray(value) || value instanceof List; + return Array.isArray(value) || value instanceof lazy.List; } - -/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- We define these once to avoid using "any" through the code */ -export type AnyList = List; - -// Injections needed to break circular dependencies -Results.List = List; -Results.createListAccessor = createListAccessor; diff --git a/packages/realm/src/collection-accessors/OrderedCollection.ts b/packages/realm/src/collection-accessors/OrderedCollection.ts new file mode 100644 index 00000000000..39f079e0016 --- /dev/null +++ b/packages/realm/src/collection-accessors/OrderedCollection.ts @@ -0,0 +1,53 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +import { binding } from "../../binding"; +import type { OrderedCollectionInternal } from "../OrderedCollection"; +import type { TypeHelpers } from "../TypeHelpers"; + +type Getter = (collection: CollectionType, index: number) => T; + +type GetterFactoryOptions = { + fromBinding: TypeHelpers["fromBinding"]; + itemType: binding.PropertyType; +}; + +/** @internal */ +export function createDefaultGetter({ + fromBinding, + itemType, +}: GetterFactoryOptions): Getter { + const isObjectItem = itemType === binding.PropertyType.Object || itemType === binding.PropertyType.LinkingObjects; + return isObjectItem ? (...args) => getObject(fromBinding, ...args) : (...args) => getKnownType(fromBinding, ...args); +} + +function getObject( + fromBinding: TypeHelpers["fromBinding"], + collection: OrderedCollectionInternal, + index: number, +): T { + return fromBinding(collection.getObj(index)); +} + +function getKnownType( + fromBinding: TypeHelpers["fromBinding"], + collection: OrderedCollectionInternal, + index: number, +): T { + return fromBinding(collection.getAny(index)); +} diff --git a/packages/realm/src/collection-accessors/Results.ts b/packages/realm/src/collection-accessors/Results.ts new file mode 100644 index 00000000000..1d14aeea488 --- /dev/null +++ b/packages/realm/src/collection-accessors/Results.ts @@ -0,0 +1,73 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +import { binding } from "../../binding"; +import { lazy } from "../lazy"; +import { createDictionaryAccessor } from "./Dictionary"; +import { createListAccessor } from "./List"; +import { createDefaultGetter } from "./OrderedCollection"; +import type { TypeHelpers } from "../TypeHelpers"; + +export type ResultsAccessor = { + get: (results: binding.Results, index: number) => T; +}; + +type ResultsAccessorFactoryOptions = { + realm: Realm; + typeHelpers: TypeHelpers; + itemType: binding.PropertyType; +}; + +/** @internal */ +export function createResultsAccessor(options: ResultsAccessorFactoryOptions): ResultsAccessor { + return options.itemType === binding.PropertyType.Mixed + ? createResultsAccessorForMixed(options) + : createResultsAccessorForKnownType(options); +} + +function createResultsAccessorForMixed({ + realm, + typeHelpers, +}: Omit, "itemType">): ResultsAccessor { + return { + get(results, index) { + const value = results.getAny(index); + switch (value) { + case binding.ListSentinel: { + const accessor = createListAccessor({ realm, typeHelpers, itemType: binding.PropertyType.Mixed }); + return new lazy.List(realm, results.getList(index), accessor, typeHelpers) as T; + } + case binding.DictionarySentinel: { + const accessor = createDictionaryAccessor({ realm, typeHelpers, itemType: binding.PropertyType.Mixed }); + return new lazy.Dictionary(realm, results.getDictionary(index), accessor, typeHelpers) as T; + } + default: + return typeHelpers.fromBinding(value); + } + }, + }; +} + +function createResultsAccessorForKnownType({ + typeHelpers, + itemType, +}: Omit, "realm">): ResultsAccessor { + return { + get: createDefaultGetter({ fromBinding: typeHelpers.fromBinding, itemType }), + }; +} diff --git a/packages/realm/src/collection-accessors/Set.ts b/packages/realm/src/collection-accessors/Set.ts new file mode 100644 index 00000000000..240ec6c8d48 --- /dev/null +++ b/packages/realm/src/collection-accessors/Set.ts @@ -0,0 +1,101 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +import { binding } from "../../binding"; +import { assert } from "../assert"; +import type { TypeHelpers } from "../TypeHelpers"; +import type { Realm } from "../Realm"; +import { createDefaultGetter } from "./OrderedCollection"; + +export type SetAccessor = { + get: (set: binding.Set, index: number) => T; + set: (set: binding.Set, index: number, value: T) => void; + insert: (set: binding.Set, value: T) => void; +}; + +type SetAccessorFactoryOptions = { + realm: Realm; + typeHelpers: TypeHelpers; + itemType: binding.PropertyType; +}; + +/** @internal */ +export function createSetAccessor(options: SetAccessorFactoryOptions): SetAccessor { + return options.itemType === binding.PropertyType.Mixed + ? createSetAccessorForMixed(options) + : createSetAccessorForKnownType(options); +} + +function createSetAccessorForMixed({ + realm, + typeHelpers, +}: Omit, "itemType">): SetAccessor { + const { fromBinding, toBinding } = typeHelpers; + return { + get(set, index) { + // Core will not return collections within a Set. + return fromBinding(set.getAny(index)); + }, + // Directly setting by "index" to a Set is a no-op. + set: () => {}, + insert(set, value) { + assert.inTransaction(realm); + + try { + set.insertAny(toBinding(value)); + } catch (err) { + // Optimize for the valid cases by not guarding for the unsupported nested collections upfront. + throw transformError(err); + } + }, + }; +} + +function createSetAccessorForKnownType({ + realm, + typeHelpers, + itemType, +}: SetAccessorFactoryOptions): SetAccessor { + const { fromBinding, toBinding } = typeHelpers; + return { + get: createDefaultGetter({ fromBinding, itemType }), + // Directly setting by "index" to a Set is a no-op. + set: () => {}, + insert(set, value) { + assert.inTransaction(realm); + + try { + set.insertAny(toBinding(value)); + } catch (err) { + // Optimize for the valid cases by not guarding for the unsupported nested collections upfront. + throw transformError(err); + } + }, + }; +} + +function transformError(err: unknown) { + const message = err instanceof Error ? err.message : ""; + if (message?.includes("'Array' to a Mixed") || message?.includes("'List' to a Mixed")) { + return new Error("Lists within a Set are not supported."); + } + if (message?.includes("'Object' to a Mixed") || message?.includes("'Dictionary' to a Mixed")) { + return new Error("Dictionaries within a Set are not supported."); + } + return err; +} diff --git a/packages/realm/src/lazy.ts b/packages/realm/src/lazy.ts new file mode 100644 index 00000000000..8bc8015282f --- /dev/null +++ b/packages/realm/src/lazy.ts @@ -0,0 +1,75 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +import type { Realm } from "./Realm"; +import type { Results } from "./Results"; +import type { List } from "./List"; +import type { Dictionary } from "./Dictionary"; +import type { RealmSet } from "./Set"; +import type { OrderedCollection } from "./OrderedCollection"; +import type { RealmObject } from "./Object"; +import type { Collection } from "./Collection"; +import type { App } from "./app-services/App"; +import type { User } from "./app-services/User"; + +type Lazy = { + Realm: typeof Realm; + Collection: typeof Collection; + OrderedCollection: typeof OrderedCollection; + Results: typeof Results; + List: typeof List; + Dictionary: typeof Dictionary; + Set: typeof RealmSet; + Object: typeof RealmObject; + App: typeof App; + User: typeof User; +}; + +/** + * Values that can be dependent on at runtime without eagerly loading it into the module. + * @internal + */ +export const lazy = {} as Lazy; + +const THROW_ON_ACCESS_HANDLER: ProxyHandler = { + get(_target, prop) { + if (typeof prop === "string") { + throw new AccessError(prop); + } + }, +}; + +// Setting a prototype which throws if an lazy value gets accessed at runtime before it's injected +Object.setPrototypeOf(lazy, new Proxy({}, THROW_ON_ACCESS_HANDLER)); + +class AccessError extends Error { + constructor(name: string) { + super(`Accessing lazy ${name} before it got injected`); + } +} + +/** + * Injects a value that can be dependent on at runtime without eagerly loading it into the module. + * @internal + */ +export function injectLazy(name: Name, value: (typeof lazy)[typeof name]) { + Object.defineProperty(lazy, name, { + value, + writable: false, + }); +} diff --git a/packages/realm/src/platform/node/index.ts b/packages/realm/src/platform/node/index.ts index 87cc49e4da0..dfbef65892c 100644 --- a/packages/realm/src/platform/node/index.ts +++ b/packages/realm/src/platform/node/index.ts @@ -20,10 +20,10 @@ import "./binding"; import "./fs"; import "./device-info"; import "./sync-proxy-config"; -import "./custom-inspect"; import "./garbage-collection"; import { Realm } from "../../Realm"; export = Realm; -import "./deprecated-global"; +import "./custom-inspect"; +import "../../deprecated-global"; diff --git a/packages/realm/src/platform/react-native/index.ts b/packages/realm/src/platform/react-native/index.ts index 61e43bac4e3..5c2b9f043f0 100644 --- a/packages/realm/src/platform/react-native/index.ts +++ b/packages/realm/src/platform/react-native/index.ts @@ -31,4 +31,4 @@ binding.App.clearCachedApps(); export = Realm; -import "./deprecated-global"; +import "../../deprecated-global"; diff --git a/packages/realm/src/property-accessors/Array.ts b/packages/realm/src/property-accessors/Array.ts index b30782f5253..2f687c2f8bd 100644 --- a/packages/realm/src/property-accessors/Array.ts +++ b/packages/realm/src/property-accessors/Array.ts @@ -18,9 +18,11 @@ import { binding } from "../../binding"; import { assert } from "../assert"; +import { createListAccessor } from "../collection-accessors/List"; +import { createResultsAccessor } from "../collection-accessors/Results"; import { TypeAssertionError } from "../errors"; -import { List, createListAccessor } from "../List"; -import { Results, createResultsAccessor } from "../Results"; +import { List } from "../List"; +import { Results } from "../Results"; import { getTypeHelpers, toItemType } from "../TypeHelpers"; import type { PropertyAccessor, PropertyOptions } from "./types"; diff --git a/packages/realm/src/property-accessors/Dictionary.ts b/packages/realm/src/property-accessors/Dictionary.ts index 1f73f90eb85..64f0819b397 100644 --- a/packages/realm/src/property-accessors/Dictionary.ts +++ b/packages/realm/src/property-accessors/Dictionary.ts @@ -19,9 +19,10 @@ import { binding } from "../../binding"; import { assert } from "../assert"; import { TypeAssertionError } from "../errors"; -import { Dictionary, createDictionaryAccessor } from "../Dictionary"; +import { Dictionary } from "../Dictionary"; import { getTypeHelpers, toItemType } from "../TypeHelpers"; import type { PropertyAccessor, PropertyOptions } from "./types"; +import { createDictionaryAccessor } from "../collection-accessors/Dictionary"; /** @internal */ export function createDictionaryPropertyAccessor({ diff --git a/packages/realm/src/property-accessors/Mixed.ts b/packages/realm/src/property-accessors/Mixed.ts index 932112c6040..e2155305e89 100644 --- a/packages/realm/src/property-accessors/Mixed.ts +++ b/packages/realm/src/property-accessors/Mixed.ts @@ -18,13 +18,14 @@ import { binding } from "../../binding"; import { assert } from "../assert"; +import { Dictionary } from "../Dictionary"; import { - Dictionary, createDictionaryAccessor, insertIntoDictionaryOfMixed, isJsOrRealmDictionary, -} from "../Dictionary"; -import { List, createListAccessor, insertIntoListOfMixed, isJsOrRealmList } from "../List"; +} from "../collection-accessors/Dictionary"; +import { List } from "../List"; +import { createListAccessor, insertIntoListOfMixed, isJsOrRealmList } from "../collection-accessors/List"; import { createDefaultPropertyAccessor } from "./default"; import type { PropertyAccessor, PropertyOptions } from "./types"; diff --git a/packages/realm/src/property-accessors/Set.ts b/packages/realm/src/property-accessors/Set.ts index c4d254654fb..940c53f048d 100644 --- a/packages/realm/src/property-accessors/Set.ts +++ b/packages/realm/src/property-accessors/Set.ts @@ -18,9 +18,10 @@ import { binding } from "../../binding"; import { assert } from "../assert"; -import { RealmSet, createSetAccessor } from "../Set"; +import { RealmSet } from "../Set"; import type { PropertyAccessor, PropertyOptions } from "./types"; import { getTypeHelpers, toItemType } from "../TypeHelpers"; +import { createSetAccessor } from "../collection-accessors/Set"; /** @internal */ export function createSetPropertyAccessor({ diff --git a/packages/realm/src/property-accessors/types.ts b/packages/realm/src/property-accessors/types.ts index cbec3261145..05c6194f0da 100644 --- a/packages/realm/src/property-accessors/types.ts +++ b/packages/realm/src/property-accessors/types.ts @@ -18,7 +18,7 @@ import type { binding } from "../../binding"; import type { ClassHelpers } from "../ClassHelpers"; -import type { ListAccessor } from "../List"; +import type { ListAccessor } from "../collection-accessors/List"; import type { Realm } from "../Realm"; import type { PresentationPropertyTypeName } from "../schema"; import type { TypeHelpers } from "../TypeHelpers"; diff --git a/packages/realm/src/schema/validate.ts b/packages/realm/src/schema/validate.ts index 9078dade62d..296f1f61c85 100644 --- a/packages/realm/src/schema/validate.ts +++ b/packages/realm/src/schema/validate.ts @@ -18,6 +18,7 @@ import { assert } from "../assert"; import { ObjectSchemaParseError, PropertySchemaParseError } from "../errors"; +import { lazy } from "../lazy"; import type { CanonicalObjectSchema, CanonicalPropertySchema, @@ -27,7 +28,6 @@ import type { RealmObjectConstructor, } from "../schema"; import type { Configuration } from "../Configuration"; -import { RealmObject } from "../Object"; // Need to use `CanonicalObjectSchema` rather than `ObjectSchema` due to some // integration tests using `openRealmHook()`. That function sets `this.realm` @@ -80,7 +80,7 @@ export function validateObjectSchema( if (typeof objectSchema === "function") { const clazz = objectSchema as unknown as DefaultObject; // We assert this later, but want a custom error message - if (!(objectSchema.prototype instanceof RealmObject)) { + if (!(objectSchema.prototype instanceof lazy.Object)) { const schemaName = clazz.schema && (clazz.schema as DefaultObject).name; if (typeof schemaName === "string" && schemaName !== objectSchema.name) { throw new TypeError( diff --git a/packages/realm/src/symbols.ts b/packages/realm/src/symbols.ts index 77021acc0a1..25bbe604179 100644 --- a/packages/realm/src/symbols.ts +++ b/packages/realm/src/symbols.ts @@ -17,3 +17,5 @@ //////////////////////////////////////////////////////////////////////////// export const OBJECT_INTERNAL = Symbol("Object#internal"); +export const OBJECT_REALM = Symbol("Object#realm"); +export const OBJECT_HELPERS = Symbol("Object#helpers"); diff --git a/packages/realm/src/tests/collection-helpers.test.ts b/packages/realm/src/tests/collection-helpers.test.ts index b80cd2db437..1c1496ed388 100644 --- a/packages/realm/src/tests/collection-helpers.test.ts +++ b/packages/realm/src/tests/collection-helpers.test.ts @@ -18,7 +18,7 @@ import { expect } from "chai"; -import { isPOJO } from "../Dictionary"; +import { isPOJO } from "../collection-accessors/Dictionary"; describe("Collection helpers", () => { describe("isPOJO()", () => { diff --git a/packages/realm/src/type-helpers/Array.ts b/packages/realm/src/type-helpers/Array.ts index 50a69662538..152be476346 100644 --- a/packages/realm/src/type-helpers/Array.ts +++ b/packages/realm/src/type-helpers/Array.ts @@ -18,7 +18,7 @@ import { binding } from "../../binding"; import { assert } from "../assert"; -import { List } from "../List"; +import { lazy } from "../lazy"; import type { TypeHelpers, TypeOptions } from "./types"; /** @internal */ @@ -32,7 +32,7 @@ export function createArrayTypeHelpers({ realm, getClassHelpers, name, objectSch const propertyHelpers = classHelpers.properties.get(name); const { listAccessor } = propertyHelpers; assert.object(listAccessor); - return new List(realm, value, listAccessor, propertyHelpers); + return new lazy.List(realm, value, listAccessor, propertyHelpers); }, toBinding() { throw new Error("Not supported"); diff --git a/packages/realm/src/type-helpers/Mixed.ts b/packages/realm/src/type-helpers/Mixed.ts index d5cadd7cf75..b2c8b76eca7 100644 --- a/packages/realm/src/type-helpers/Mixed.ts +++ b/packages/realm/src/type-helpers/Mixed.ts @@ -18,12 +18,9 @@ import { binding } from "../../binding"; import { assert } from "../assert"; -import { Collection } from "../Collection"; -import { Counter } from "../Counter"; -import { Dictionary, createDictionaryAccessor } from "../Dictionary"; -import { List, createListAccessor } from "../List"; -import { REALM, RealmObject } from "../Object"; -import { RealmSet } from "../Set"; +import { lazy } from "../lazy"; +import { createDictionaryAccessor } from "../collection-accessors/Dictionary"; +import { createListAccessor } from "../collection-accessors/List"; import { boxToBindingGeospatial, circleToBindingGeospatial, @@ -32,8 +29,9 @@ import { isGeoPolygon, polygonToBindingGeospatial, } from "../GeoSpatial"; +import { Counter } from "../Counter"; import { getTypeHelpers } from "../TypeHelpers"; -import { OBJECT_INTERNAL } from "../symbols"; +import { OBJECT_INTERNAL, OBJECT_REALM } from "../symbols"; import { TYPED_ARRAY_CONSTRUCTORS } from "./array-buffer"; import type { TypeHelpers, TypeOptions } from "./types"; @@ -63,14 +61,14 @@ export function mixedToBinding( return null; } else if (value instanceof Date) { return binding.Timestamp.fromDate(value); - } else if (value instanceof RealmObject) { + } else if (value instanceof lazy.Object) { if (value.objectSchema().embedded) { throw new Error(`Using an embedded object (${value.constructor.name}) as ${displayedType} is not supported.`); } - const otherRealm = value[REALM].internal; + const otherRealm = value[OBJECT_REALM].internal; assert.isSameRealm(realm, otherRealm, "Realm object is from another Realm"); return value[OBJECT_INTERNAL]; - } else if (value instanceof RealmSet || value instanceof Set) { + } else if (value instanceof lazy.Set || value instanceof Set) { throw new Error(`Using a ${value.constructor.name} as ${displayedType} is not supported.`); } else if (value instanceof Counter) { let errMessage = `Using a Counter as ${displayedType} is not supported.`; @@ -78,7 +76,7 @@ export function mixedToBinding( throw new Error(errMessage); } else { if (isQueryArg) { - if (value instanceof Collection || Array.isArray(value)) { + if (value instanceof lazy.Collection || Array.isArray(value)) { throw new Error(`Using a ${value.constructor.name} as a query argument is not supported.`); } // Geospatial types can currently only be used when querying and @@ -123,11 +121,11 @@ function mixedFromBinding(options: TypeOptions, value: binding.MixedArg): unknow } else if (value instanceof binding.List) { const mixedType = binding.PropertyType.Mixed; const typeHelpers = getTypeHelpers(mixedType, options); - return new List(realm, value, createListAccessor({ realm, typeHelpers, itemType: mixedType }), typeHelpers); + return new lazy.List(realm, value, createListAccessor({ realm, typeHelpers, itemType: mixedType }), typeHelpers); } else if (value instanceof binding.Dictionary) { const mixedType = binding.PropertyType.Mixed; const typeHelpers = getTypeHelpers(mixedType, options); - return new Dictionary( + return new lazy.Dictionary( realm, value, createDictionaryAccessor({ realm, typeHelpers, itemType: mixedType }), diff --git a/packages/realm/src/type-helpers/Object.ts b/packages/realm/src/type-helpers/Object.ts index 4c7b7e07caf..b975ec2e5dc 100644 --- a/packages/realm/src/type-helpers/Object.ts +++ b/packages/realm/src/type-helpers/Object.ts @@ -18,8 +18,8 @@ import { binding } from "../../binding"; import { assert } from "../assert"; -import { REALM, RealmObject, UpdateMode } from "../Object"; -import { OBJECT_INTERNAL } from "../symbols"; +import { RealmObject, UpdateMode } from "../Object"; +import { OBJECT_INTERNAL, OBJECT_REALM } from "../symbols"; import { nullPassthrough } from "./null-passthrough"; import type { TypeHelpers, TypeOptions } from "./types"; @@ -39,7 +39,7 @@ export function createObjectTypeHelpers({ if ( value instanceof RealmObject && value.constructor.name === objectType && - value[REALM].internal.$addr === realm.internal.$addr + value[OBJECT_REALM].internal.$addr === realm.internal.$addr ) { return value[OBJECT_INTERNAL]; } else {