diff --git a/.github/workflows/pr-realm-js.yml b/.github/workflows/pr-realm-js.yml index e0777696f3..f9da06dc17 100644 --- a/.github/workflows/pr-realm-js.yml +++ b/.github/workflows/pr-realm-js.yml @@ -88,8 +88,8 @@ jobs: - name: Install dependencies # Ignoring scripts to prevent a prebuilt from getting fetched / built run: npm ci --ignore-scripts - - name: Bundle all packages - run: npm run bundle + - name: Build / bundle all packages + run: npm run build - name: Upload dist artifacts uses: actions/upload-artifact@v4 with: diff --git a/.gitignore b/.gitignore index 358c266777..06fb301538 100644 --- a/.gitignore +++ b/.gitignore @@ -103,12 +103,11 @@ coverage/ # Generated artifacts /packages/realm/bindgen/vendor/bindgen-lib/generated/ /packages/realm/generated/ -/packages/realm/binding/generated /packages/realm/binding/jsi/jsi_init.cpp +/packages/realm/src/**/*.generated.ts # Build artifacts /packages/realm/dist/ -/packages/realm/binding/dist/ /packages/realm/binding/build/ /packages/realm/binding/android/build/ /packages/realm/binding/node/build/ diff --git a/integration-tests/tests/package.json b/integration-tests/tests/package.json index aa9468ae80..28826358f6 100644 --- a/integration-tests/tests/package.json +++ b/integration-tests/tests/package.json @@ -18,15 +18,12 @@ }, "wireit": { "lint": { - "command": "eslint --ext .js,.ts . && tsc --noEmit", - "dependencies": [ - "build-dependencies" - ] + "command": "eslint --ext .js,.ts ." }, "build": { "command": "tsc --build .", "dependencies": [ - "build-dependencies" + "../../packages/realm:build:ts" ] }, "start": { diff --git a/package.json b/package.json index 7cce0e1560..f8eb138298 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "homepage": "https://www.mongodb.com/docs/realm/", "scripts": { "build": "wireit", - "bundle": "wireit", "lint": "wireit", "lint:fix": "wireit", "clean": "git clean -fdx -e node_modules -e .env", @@ -15,19 +14,14 @@ }, "wireit": { "build": { - "dependencies": [ - "bundle", - "./packages/realm:build:ts" - ] - }, - "bundle": { "dependencies": [ "./packages/realm:build:ts", "./packages/babel-plugin:bundle", "./packages/mocha-reporter:bundle", "./packages/realm-react:bundle", "./packages/realm-web:bundle", - "./packages/realm-tools:build" + "./packages/realm-tools:build", + "./integration-tests/tests:build" ] }, "lint": { diff --git a/packages/babel-plugin/package.json b/packages/babel-plugin/package.json index ee3d4392a5..66291ed5cb 100644 --- a/packages/babel-plugin/package.json +++ b/packages/babel-plugin/package.json @@ -23,8 +23,7 @@ "test": { "command": "jest", "dependencies": [ - "../realm:build:ts", - "../realm:bindgen:build:typescript" + "../realm:build:ts" ] } }, diff --git a/packages/realm/bindgen/src/templates/base-wrapper.ts b/packages/realm/bindgen/src/templates/base-wrapper.ts deleted file mode 100644 index d50a8cdea1..0000000000 --- a/packages/realm/bindgen/src/templates/base-wrapper.ts +++ /dev/null @@ -1,162 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2022 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 { strict as assert } from "assert"; -import { Property } from "@realm/bindgen/bound-model"; -import { TemplateContext } from "@realm/bindgen/context"; -import { Outputter } from "@realm/bindgen/outputter"; - -import { doJsPasses } from "../js-passes"; - -export function generateNativeBigIntSupport(out: Outputter) { - out(` - const NativeBigIntSupport = Object.freeze({ - add(a, b) { return a + b; }, - equals(a, b) { return a == b; }, // using == rather than === to support number and string RHS! - isInt(a) { return typeof(a) === "bigint"; }, - numToInt(a) { return BigInt(a); }, - strToInt(a) { return BigInt(a); }, - intToNum(a) { return Number(a); }, - }); - `); -} - -export function generate({ spec: boundSpec }: TemplateContext, out: Outputter): void { - const spec = doJsPasses(boundSpec); - - out(` - import { Long, ObjectId, UUID, Decimal128, EJSON } from "bson"; - import { Float, Status } from "../dist/core"; - - export * from "../dist/core"; - - // Copied from lib/utils.js. - // TODO consider importing instead. - // Might be slightly faster to make dedicated wrapper for 1 and 2 argument forms, but unlikely to be worth it. - function _promisify(nullAllowed, func) { - return new Promise((resolve, reject) => { - func((...cbargs) => { - // Any errors in this function should flow into the Promise chain, rather than out to the caller, - // since callers of async callbacks aren't expecting exceptions. - try { - if (cbargs.length < 1 || cbargs.length > 2) throw Error("invalid cbargs length " + cbargs.length); - let error = cbargs[cbargs.length - 1]; - if (error) { - reject(error); - } else if (cbargs.length == 2) { - const result = cbargs[0]; - if (!nullAllowed && (result === null || result === undefined)) { - throw new Error("Unexpected null or undefined successful result"); - } - resolve(result); - } else { - resolve(); - } - } catch (err) { - reject(err); - } - }); - }); - } - `); - - const injectables = [ - "Long", - "ArrayBuffer", - "Float", - "Status", - "ObjectId", - "UUID", - "Decimal128", - "EJSON_parse: EJSON.parse", - "EJSON_stringify: EJSON.stringify", - "Symbol_for: Symbol.for", - ]; - - for (const cls of spec.classes) { - injectables.push(cls.jsName); - - let body = ""; - - // It will always be accessed via this name rather than a static to enable optimizations - // that depend on the symbol not changing. - const symb = `_${cls.rootBase().jsName}_Symbol`; - - if (!cls.base) { - // Only root classes get symbols and constructors - out(`const ${symb} = Symbol("Realm.${cls.jsName}.external_pointer");`); - - body += `constructor(ptr) { this[${symb}] = ptr};`; - } - - // This will override the extractor from the base class to do a more specific type check. - body += ` - static _extract(self) { - if (!(self instanceof ${cls.jsName})) - throw new TypeError("Expected a ${cls.jsName}"); - const out = self[${symb}]; - if (!out) - throw new TypeError("received an improperly constructed ${cls.jsName}"); - return out; - }; - `; - - for (const method of cls.methods) { - if (!method.isOptedInTo) continue; - - // Eagerly bind the name once from the native module. - const native = `_native_${method.id}`; - out(`const ${native} = nativeModule.${method.id};`); - // TODO consider pre-extracting class-typed arguments while still in JIT VM. - const asyncSig = method.sig.asyncTransform(); - const params = (asyncSig ?? method.sig).args.map((a) => a.name); - const args = [ - method.isStatic ? [] : `this[${symb}]`, // - ...params, - asyncSig ? "_cb" : [], - ].flat(); - let call = `${native}(${args})`; - if (asyncSig) { - // JS can't distinguish between a `const EJson*` that is nullptr (which can't happen), and - // one that points to the string "null" because both become null by the time they reach JS. - // In order to allow the latter (which does happen! E.g. the promise from `response.text()` - // can resolve to `"null"`) we need a special case here. - // TODO see if there is a better approach. - assert(asyncSig.ret.isTemplate("AsyncResult")); - const ret = asyncSig.ret.args[0]; - const nullAllowed = !!(ret.is("Pointer") && ret.type.kind == "Const" && ret.type.type.isPrimitive("EJson")); - call = `_promisify(${nullAllowed}, _cb => ${call})`; - } - body += ` - ${method.isStatic ? "static" : ""} - ${method instanceof Property ? "get" : ""} - ${method.jsName}(${params}) { - return ${call}; - }`; - } - - if (cls.iterable) { - const native = `_native_${cls.iteratorMethodId()}`; - out(`const ${native} = nativeModule.${cls.iteratorMethodId()};`); - body += `\n[Symbol.iterator]() { return ${native}(this[${symb}]); }`; - } - - out(`export class ${cls.jsName} ${cls.base ? `extends ${cls.base.jsName}` : ""} { ${body} }`); - } - - out(`nativeModule.injectInjectables({ ${injectables} });`); -} diff --git a/packages/realm/bindgen/src/templates/node-wrapper.ts b/packages/realm/bindgen/src/templates/node-wrapper.ts deleted file mode 100644 index 0c7cb558a2..0000000000 --- a/packages/realm/bindgen/src/templates/node-wrapper.ts +++ /dev/null @@ -1,51 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2022 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 { TemplateContext } from "@realm/bindgen/context"; - -import { eslintFormatter } from "../formatters"; -import { generate as generateBase, generateNativeBigIntSupport } from "./base-wrapper"; - -export function generate(context: TemplateContext): void { - const out = context.file("native.node.mjs", eslintFormatter); - - out("// This file is generated: Update the spec instead of editing this file directly"); - - out(` - /* eslint-disable @typescript-eslint/no-var-requires */ - /* global global, require */ - const nativeModule = require("#realm.node"); - - if(!nativeModule) { - throw new Error("Could not find the Realm binary. Please consult our troubleshooting guide: https://www.mongodb.com/docs/realm-sdks/js/latest/#md:troubleshooting-missing-binary"); - } - - // We know that node always has real WeakRefs so just use them. - export const WeakRef = global.WeakRef; - `); - - generateNativeBigIntSupport(out); - - out(` - export const Int64 = NativeBigIntSupport; // Node always supports BigInt - `); - - generateBase(context, out); - - context.file("native.node.d.mts", eslintFormatter)("export * from './native'"); - context.file("native.node.d.cts", eslintFormatter)("import * as binding from './native'; export = binding;"); -} diff --git a/packages/realm/bindgen/src/templates/react-native-wrapper.ts b/packages/realm/bindgen/src/templates/react-native-wrapper.ts deleted file mode 100644 index 977195b2dd..0000000000 --- a/packages/realm/bindgen/src/templates/react-native-wrapper.ts +++ /dev/null @@ -1,76 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2022 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 { TemplateContext } from "@realm/bindgen/context"; - -import { eslintFormatter } from "../formatters"; -import { generate as generateBase, generateNativeBigIntSupport } from "./base-wrapper"; - -export function generate(context: TemplateContext): void { - const out = context.file("native.react-native.mjs", eslintFormatter); - - out("// This file is generated: Update the spec instead of editing this file directly"); - - out(` - /* global global */ - - import { NativeModules } from "react-native"; - const RealmNativeModule = NativeModules.Realm; - - /** - * Injects, reads, deletes and returns the native module via a global property. - */ - function getNativeModule() { - RealmNativeModule.injectModuleIntoJSGlobal(); - // Read the global into the local scope - const { __injectedRealmBinding } = global; - // Delete the global again - delete global.__injectedRealmBinding; - if(typeof __injectedRealmBinding === "object") { - return __injectedRealmBinding; - } else { - throw new Error("Could not find the Realm binary. Please consult our troubleshooting guide: https://www.mongodb.com/docs/realm-sdks/js/latest/#md:troubleshooting-missing-binary"); - } - } - - const nativeModule = getNativeModule(); - - export const WeakRef = global.WeakRef ?? class WeakRef { - constructor(obj) { this.native = nativeModule.createWeakRef(obj) } - deref() { return nativeModule.lockWeakRef(this.native) } - }; - `); - - generateNativeBigIntSupport(out); - - out(` - // Hermes supports BigInt, but JSC doesn't. - export const Int64 = global.HermesInternal ? NativeBigIntSupport : { - add(a, b) { return a.add(b); }, - equals(a, b) { return a.equals(b); }, - isInt(a) { return a instanceof Long; }, - numToInt(a) { return Long.fromNumber(a); }, - strToInt(a) { return Long.fromString(a); }, - intToNum(a) { return a.toNumber(); }, - } - `); - - generateBase(context, out); - - context.file("native.react-native.d.mts", eslintFormatter)("export * from './native'"); - context.file("native.react-native.d.cts", eslintFormatter)("import * as binding from './native'; export = binding;"); -} diff --git a/packages/realm/bindgen/src/templates/typescript.ts b/packages/realm/bindgen/src/templates/typescript.ts deleted file mode 100644 index 44de3541e4..0000000000 --- a/packages/realm/bindgen/src/templates/typescript.ts +++ /dev/null @@ -1,293 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2022 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 { strict as assert } from "node:assert"; - -import { TemplateContext } from "@realm/bindgen/context"; -import { Arg, BoundSpec, NamedType, Property, Type } from "@realm/bindgen/bound-model"; - -import { doJsPasses } from "../js-passes"; -import { eslintFormatter } from "../formatters"; - -const PRIMITIVES_MAPPING: Record = { - void: "void", - bool: "boolean", - double: "number", - float: "Float", - int64_t: "Int64", - int32_t: "number", - count_t: "number", - uint64_t: "Int64", - "std::chrono::milliseconds": "Int64", - "std::string": "string", - "std::string_view": "string", - StringData: "string", - EncryptionKey: "ArrayBuffer", - BinaryData: "ArrayBuffer", - OwnedBinaryData: "ArrayBuffer", - ObjectId: "ObjectId", - UUID: "UUID", - Decimal128: "Decimal128", - AppError: "AppError", - "std::exception_ptr": "Error", - "std::error_code": "CppErrorCode", - Status: "Status", - EJson: "EJson", - EJsonArray: "EJson[]", - EJsonObj: "Record", - "bson::BsonDocument": "Record", - "bson::BsonArray": "EJson[]", - QueryArg: "(MixedArg | MixedArg[])", - "std::uint_fast16_t": "number", -}; - -// Be Careful! These need to apply to the *whole* type, so arg[] would be problematic if arg is A|B. -const TEMPLATE_MAPPING: Record string> = { - "std::vector": (arg) => `Array<${arg}>`, - "std::optional": (arg) => `undefined | ${arg}`, - Nullable: (t) => `null | ${t}`, - "std::shared_ptr": (arg) => arg, - "std::pair": (a, b) => `[${a}, ${b}]`, - "std::tuple": (...args) => `[${args}]`, - "std::map": (k, v) => `Record<${k}, ${v}>`, - "std::unordered_map": (k, v) => `Record<${k}, ${v}>`, - "util::UniqueFunction": (f) => f, - "std::function": (f) => f, - AsyncResult: (t) => `Promise<${t}>`, - AsyncCallback: (sig) => assert.fail(`async transform not applied to function taking AsyncCallback<${sig}>`), - IgnoreArgument: () => assert.fail("Attempting to use an IgnoreArgument<>"), -}; - -const enum Kind { - Arg, // JS -> CPP - Ret, // Cpp -> JS -} -function suffix(kind: Kind) { - return kind === Kind.Arg ? "_Relaxed" : ""; -} - -function generateType(spec: BoundSpec, type: Type, kind: Kind): string { - switch (type.kind) { - case "Pointer": - case "Ref": - case "RRef": - // No impact on JS semantics. - return generateType(spec, type.type, kind); - - case "Const": - return `Readonly<${generateType(spec, type.type, kind)}>`; - - case "KeyType": - case "Opaque": - case "Enum": - case "Class": - return type.jsName; - - case "Struct": - return type.jsName + suffix(kind); - - case "Primitive": - if (type.name === "Mixed") return kind === Kind.Arg ? "MixedArg" : "Mixed"; - return PRIMITIVES_MAPPING[type.name]; - - case "Template": - return TEMPLATE_MAPPING[type.name](...type.args.map((arg) => generateType(spec, arg, kind))); - - case "Func": - // When a js function is passed to cpp, its arguments behave like Ret and its return value behaves like Arg. - const Arg = kind === Kind.Arg ? Kind.Ret : Kind.Arg; - const Ret = kind === Kind.Arg ? Kind.Arg : Kind.Ret; - - const args = type.argsSkippingIgnored().map((arg) => arg.name + ": " + generateType(spec, arg.type, Arg)); - return `((${args.join(", ")}) => ${generateType(spec, type.ret, Ret)})`; - } -} - -function generateArguments(spec: BoundSpec, args: Arg[]) { - return args.map((arg) => `${arg.name}: ${generateType(spec, arg.type, Kind.Arg)}`).join(", "); -} - -function generateMixedTypes(spec: BoundSpec) { - return ` - export type Mixed = null | symbol | ${spec.mixedInfo.getters - .map(({ type }) => generateType(spec, type, Kind.Ret)) - .join(" | ")}; - export type MixedArg = null | ${spec.mixedInfo.ctors.map((type) => generateType(spec, type, Kind.Arg)).join(" | ")}; - `; -} - -export function generate({ rawSpec, spec: boundSpec, file }: TemplateContext): void { - // Check the support for primitives used - for (const primitive of rawSpec.primitives) { - if (primitive === "Mixed") continue; - assert( - Object.keys(PRIMITIVES_MAPPING).includes(primitive), - `Spec declares an unsupported primitive: "${primitive}"`, - ); - } - - // Check the support for template instances used - for (const template of Object.keys(rawSpec.templates)) { - assert( - Object.keys(TEMPLATE_MAPPING).includes(template), - `Spec declares an unsupported template instance: "${template}"`, - ); - } - - const spec = doJsPasses(boundSpec); - - const coreOut = file("core.ts", eslintFormatter); - coreOut("// This file is generated: Update the spec instead of editing this file directly"); - - coreOut("// Enums"); - for (const e of spec.enums) { - // Using const enum to avoid having to emit JS backing these - coreOut(`export const enum ${e.jsName} {`); - coreOut(...e.enumerators.map(({ jsName, value }) => `${jsName} = ${value},\n`)); - coreOut("};"); - } - coreOut(` - // Wrapped types - export class Float { - constructor(public value: number) {} - valueOf() { return this.value; } - } - export class Status { - public isOk: boolean; - public code?: number; - public reason?: string; - constructor(isOk: boolean) { this.isOk = isOk; } - } - export const ListSentinel = Symbol.for("Realm.List"); - export const DictionarySentinel = Symbol.for("Realm.Dictionary"); - `); - - const out = file("native.d.ts", eslintFormatter); - out("// This file is generated: Update the spec instead of editing this file directly"); - out('import { ObjectId, UUID, Decimal128 } from "bson";'); - out("import { Float, Status, ", spec.enums.map((e) => e.name).join(", "), '} from "../dist/core";'); - out('export * from "../dist/core";'); - - out("// Utilities"); - out("export type AppError = Error & {code: number};"); - out("export type CppErrorCode = Error & {code: number, category: string};"); - - out(` - // WeakRef polyfill for Hermes. - export class WeakRef { - constructor(obj: T); - deref(): T | undefined; - } - - export const enum Int64Type {} // This shouldn't need to be exported, but rollup complains if it isn't. - export type Int64 = Int64Type; - export const Int64: { - add(a: Int64, b: Int64): Int64; - equals(a: Int64, b: Int64 | number | string): boolean; - isInt(a: unknown): a is Int64; - numToInt(a: number): Int64; - strToInt(a: string): Int64; - intToNum(a: Int64): number; - } - `); - - out("// Mixed types"); - out(generateMixedTypes(spec)); - out("export type EJson = null | string | number | boolean | EJson[] | {[name: string]: EJson}"); - - out("// Opaque types (including Key types)"); - for (const { jsName } of (spec.opaqueTypes as NamedType[]).concat(spec.keyTypes)) { - out.lines("/** Using an empty enum to express a nominal type */", `export enum ${jsName} {}`); - } - - out("// Records"); - for (const rec of spec.records) { - for (const kind of [Kind.Ret, Kind.Arg]) { - // Skip function fields when converting records from C++ to JS. - const fields = kind === Kind.Arg ? rec.fields : rec.fields.filter((field) => !field.type.isFunction()); - - if (fields.length === 0) { - // ESLint complains if we use {} as a type, which would otherwise happen in this case. - out(`export type ${rec.jsName}${suffix(kind)} = Record;`); - continue; - } - - // TODO consider making the Arg version just alias the Ret version if the bodies are the same. - out(`export type ${rec.jsName}${suffix(kind)} = {`); - for (const field of fields) { - // For optional fields, the field will always be there in Ret mode, but it may be undefined. - // This is handled by optional becoming `undefined | T`. - const optField = !field.required && kind === Kind.Arg; - const hasInterestingDefault = ![undefined, "", "{}", "[]"].includes(field.defaultVal); - - const docLines: string[] = []; - if (!field.isOptedInTo) { - docLines.push( - `@deprecated Add \`${field.name}\` to your opt-in list (under \`records/${rec.name}/fields/\`) to use this.`, - ); - } - if (hasInterestingDefault) { - docLines.push(`@default ${field.defaultVal}`); - } - - let docComment = ""; - if (docLines.length > 1) { - docComment = `/**\n * ${docLines.join("\n * ")}\n */\n`; - } else if (docLines.length === 1) { - docComment = `/** ${docLines[0]} */\n`; - } - - out(docComment, field.jsName, optField ? "?" : "", ": ", generateType(spec, field.type, kind), ";"); - } - out(`}`); - } - } - - out("// Classes"); - for (const cls of spec.classes) { - out(`export class ${cls.jsName} ${cls.base ? `extends ${cls.base.jsName}` : ""} {`); - out(`private brandFor${cls.jsName};`); - out(`${cls.subclasses.length === 0 ? "private" : "protected"} constructor();`); - for (const meth of cls.methods) { - if (!meth.isOptedInTo) { - out( - `/** @deprecated Add \`${meth.unique_name}\` to your opt-in list (under \`classes/${cls.name}/methods/\`) to use this. */`, - ); - } - if (meth instanceof Property) { - out("readonly ", meth.jsName, ": ", generateType(spec, meth.type, Kind.Ret)); - continue; - } - const transformedSig = meth.sig.asyncTransformOrSelf(); - const ret = generateType(spec, transformedSig.ret, Kind.Ret); - out( - meth.isStatic ? "static" : "", - meth.jsName, - "(", - generateArguments(spec, transformedSig.args), - "):", - ret, - ";", - ); - } - if (cls.iterable) { - out(`[Symbol.iterator](): Iterator<${generateType(spec, cls.iterable, Kind.Ret)}>;`); - } - out(`}`); - } -} diff --git a/packages/realm/bindgen/src/templates/wrapper.ts b/packages/realm/bindgen/src/templates/wrapper.ts new file mode 100644 index 0000000000..55624b425d --- /dev/null +++ b/packages/realm/bindgen/src/templates/wrapper.ts @@ -0,0 +1,404 @@ +import { TemplateContext } from "@realm/bindgen/context"; + +import { BoundSpec, Class, Enum, Method, NamedType, Property, Struct, Type } from "@realm/bindgen/bound-model"; +import assert from "node:assert"; +import { eslintFormatter } from "../formatters"; +import { doJsPasses } from "../js-passes"; + +function generateEnumDeclaration(e: Enum) { + return `export const enum ${e.jsName} { ${e.enumerators.map(({ jsName, value }) => `${jsName} = ${value}`)} };`; +} + +const PRIMITIVES_MAPPING: Record = { + void: "void", + bool: "boolean", + double: "number", + float: "Float", + int64_t: "Int64", + int32_t: "number", + count_t: "number", + uint64_t: "Int64", + "std::chrono::milliseconds": "Int64", + "std::string": "string", + "std::string_view": "string", + StringData: "string", + EncryptionKey: "ArrayBuffer", + BinaryData: "ArrayBuffer", + OwnedBinaryData: "ArrayBuffer", + ObjectId: "ObjectId", + UUID: "UUID", + Decimal128: "Decimal128", + AppError: "AppError", + "std::exception_ptr": "Error", + "std::error_code": "CppErrorCode", + Status: "Status", + EJson: "EJson", + EJsonArray: "EJson[]", + EJsonObj: "Record", + "bson::BsonDocument": "Record", + "bson::BsonArray": "EJson[]", + QueryArg: "(MixedArg | MixedArg[])", + "std::uint_fast16_t": "number", +}; + +const enum Kind { + Argument, // JS -> CPP + Return, // Cpp -> JS +} + +function suffix(kind: Kind) { + return kind === Kind.Argument ? "_Relaxed" : ""; +} + +function getTypeFromPrimitive(name: string) { + const result = PRIMITIVES_MAPPING[name]; + assert(typeof result === "string", `Expected a mapping for '${name}' primitive`); + return result; +} + +// Be Careful! These need to apply to the *whole* type, so arg[] would be problematic if arg is A|B. +const TEMPLATE_MAPPING: Record string) | undefined> = { + "std::vector": (arg) => `Array<${arg}>`, + "std::optional": (arg) => `undefined | ${arg}`, + Nullable: (t) => `null | ${t}`, + "std::shared_ptr": (arg) => arg, + "std::pair": (a, b) => `[${a}, ${b}]`, + "std::tuple": (...args) => `[${args}]`, + "std::map": (k, v) => `Record<${k}, ${v}>`, + "std::unordered_map": (k, v) => `Record<${k}, ${v}>`, + "util::UniqueFunction": (f) => f, + "std::function": (f) => f, + AsyncResult: (t) => `Promise<${t}>`, + AsyncCallback: (sig) => assert.fail(`async transform not applied to function taking AsyncCallback<${sig}>`), + IgnoreArgument: () => assert.fail("Attempting to use an IgnoreArgument<>"), +}; + +function getTypeFactoryFromTemplate(name: string) { + const result = TEMPLATE_MAPPING[name]; + assert(typeof result === "function", `Expected a mapping for '${name}' template`); + return result; +} + +function generateType(spec: BoundSpec, type: Type, kind: Kind): string { + switch (type.kind) { + case "Pointer": + case "Ref": + case "RRef": + // No impact on JS semantics. + return generateType(spec, type.type, kind); + + case "Const": + return `Readonly<${generateType(spec, type.type, kind)}>`; + + case "KeyType": + case "Opaque": + case "Enum": + case "Class": + return type.jsName; + + case "Struct": + return type.jsName + suffix(kind); + + case "Primitive": + if (type.name === "Mixed") return kind === Kind.Argument ? "MixedArg" : "Mixed"; + return getTypeFromPrimitive(type.name); + + case "Template": + return getTypeFactoryFromTemplate(type.name)(...type.args.map((arg) => generateType(spec, arg, kind))); + + case "Func": + // When a js function is passed to cpp, its arguments behave like Ret and its return value behaves like Arg. + const Arg = kind === Kind.Argument ? Kind.Return : Kind.Argument; + const Ret = kind === Kind.Argument ? Kind.Argument : Kind.Return; + + const args = type.argsSkippingIgnored().map((arg) => arg.name + ": " + generateType(spec, arg.type, Arg)); + return `((${args.join(", ")}) => ${generateType(spec, type.ret, Ret)})`; + } +} + +function generateRecordDeclaration(spec: BoundSpec, record: Struct) { + return [Kind.Return, Kind.Argument] + .flatMap((kind) => { + // Skip function fields when converting records from C++ to JS. + const fields = kind === Kind.Argument ? record.fields : record.fields.filter((field) => !field.type.isFunction()); + + if (fields.length === 0) { + // ESLint complains if we use {} as a type, which would otherwise happen in this case. + return `export type ${record.jsName}${suffix(kind)} = Record;`; + } + + // TODO consider making the Arg version just alias the Ret version if the bodies are the same. + const fieldDeclarations = record.fields.map((field) => { + // For optional fields, the field will always be there in Ret mode, but it may be undefined. + // This is handled by optional becoming `undefined | T`. + const optField = !field.required && kind === Kind.Argument; + const hasInterestingDefault = ![undefined, "", "{}", "[]"].includes(field.defaultVal); + + const docLines: string[] = []; + if (!field.isOptedInTo) { + docLines.push( + `@deprecated Add '${field.name}' to your opt-in list (under 'records/${record.name}/fields/') to use this.`, + ); + } + + if (hasInterestingDefault) { + docLines.push(`@default ${field.defaultVal}`); + } + + let docComment = ""; + if (docLines.length > 1) { + docComment = `/**\n * ${docLines.join("\n * ")}\n */\n`; + } else if (docLines.length === 1) { + docComment = `/** ${docLines[0]} */\n`; + } + + return `${docComment} ${field.jsName}${optField ? "?" : ""}: ${generateType(spec, field.type, kind)};`; + }); + + return `export type ${record.jsName}${suffix(kind)} = {${fieldDeclarations.join("\n")}}`; + }) + .join("\n"); +} + +function generateMethodDeclaration(spec: BoundSpec, method: Method) { + if (method instanceof Property) { + return `get ${method.jsName}(): ${generateType(spec, method.type, Kind.Return)}`; + } else { + const transformedSignature = method.sig.asyncTransformOrSelf(); + const returnType = generateType(spec, transformedSignature.ret, Kind.Return); + const argumentDeclarations = transformedSignature.args.map( + (arg) => `${arg.name}: ${generateType(spec, arg.type, Kind.Argument)}`, + ); + return (method.isStatic ? "static " : "") + method.jsName + `(${argumentDeclarations.join(", ")}): ${returnType};`; + } +} + +function generateClassDeclaration(spec: BoundSpec, cls: Class) { + const methodDeclarations = cls.methods + .filter((method) => method.isOptedInTo) + .map(generateMethodDeclaration.bind(undefined, spec)); + return ` + export declare class ${cls.jsName} ${cls.base ? `extends ${cls.base.jsName}` : ""} { + /** Opting out of structural typing */ + private brandFor${cls.jsName}; + ${cls.subclasses.length === 0 ? "private" : "protected"} constructor(); + ${methodDeclarations.join("\n")} + ${cls.iterable ? `[Symbol.iterator](): Iterator<${generateType(spec, cls.iterable, Kind.Return)}>;` : ""} + }`; +} + +/** + * Generates code wrapping the native module, making it more suitable for consumption. + */ +export function generate({ spec: boundSpec, rawSpec, file }: TemplateContext): void { + // Check the support for primitives used + for (const primitive of rawSpec.primitives) { + if (primitive === "Mixed") continue; + assert( + Object.keys(PRIMITIVES_MAPPING).includes(primitive), + `Spec declares an unsupported primitive: "${primitive}"`, + ); + } + + // Check the support for template instances used + for (const template of Object.keys(rawSpec.templates)) { + assert( + Object.keys(TEMPLATE_MAPPING).includes(template), + `Spec declares an unsupported template instance: "${template}"`, + ); + } + + const spec = doJsPasses(boundSpec); + const out = file("wrapper.generated.ts", eslintFormatter); + + out("/* this will be replaced by a header comment */"); + out("// This file is generated: Update the spec instead of editing this file directly"); + + out.lines( + 'import { Long, ObjectId, UUID, Decimal128, EJSON } from "bson";', + 'import { _promisify, _throwOnAccess } from "./utils";', + 'import * as utils from "./utils";', + 'import { applyPatch } from "./patch";', + "// eslint-disable-next-line @typescript-eslint/no-namespace", + "export namespace binding {", + ); + + out.lines("// Enums", ...spec.enums.map(generateEnumDeclaration)); + + // TODO: Attempt to move this into a proper .ts file + out.lines( + "// Utilities", + "export type AppError = Error & {code: number};", + "export type CppErrorCode = Error & {code: number, category: string};", + "export type EJson = null | string | number | boolean | EJson[] | {[name: string]: EJson}", + "export import Float = utils.Float;", + "export import Status = utils.Status;", + "export import ListSentinel = utils.ListSentinel;", + "export import DictionarySentinel = utils.DictionarySentinel;", + + ` + // WeakRef polyfill for Hermes. + export declare class WeakRef { + constructor(obj: T); + deref(): T | undefined; + } + + export declare class Int64 { + private brandForInt64; + static add(a: Int64, b: Int64): Int64; + static equals(a: Int64, b: Int64 | number | string): boolean; + static isInt(a: unknown): a is Int64; + static numToInt(a: number): Int64; + static strToInt(a: string): Int64; + static intToNum(a: Int64): number; + } + `, + ); + + out.lines( + "// Mixed types", + `export type Mixed = null | symbol | ${spec.mixedInfo.getters + .map(({ type }) => generateType(spec, type, Kind.Return)) + .join(" | ")};`, + `export type MixedArg = null | ${spec.mixedInfo.ctors.map((type) => generateType(spec, type, Kind.Argument)).join(" | ")};`, + ); + + out.lines( + "// Opaque types (including Key types)", + ...[...(spec.opaqueTypes as NamedType[]), ...spec.keyTypes].map( + ({ jsName }) => `/** Using an empty enum to express a nominal type */ export enum ${jsName} {}`, + ), + ); + + out.lines("// Records", ...spec.records.map(generateRecordDeclaration.bind(undefined, spec))); + + out.lines("// Classes", ...spec.classes.map(generateClassDeclaration.bind(undefined, spec))); + + out("}"); // Closing bracket for the namespace + + out( + ` + Object.defineProperties(binding, { + ${spec.classes.map((cls) => `${cls.jsName}: { get: _throwOnAccess.bind(undefined, "${cls.jsName}"), configurable: true }`)} + }); + `, + ); + + out( + ` + /** + * Is true when the native module has been injected. + * Useful to perform asserts on platforms which inject the native module synchronously. + */ + export let isReady = false; + + // TODO: Replace with Promise.withResolvers() once it's supported on all supported platforms. + let resolveReadyPromise: () => void = () => { throw new Error('Expected a synchronous Promise constructor'); } + /** + * Resolves when the native module has been injected. + * Useful to perform asserts on platforms which inject the native module asynchronously. + */ + export const ready = new Promise((resolve) => { resolveReadyPromise = resolve }); + `, + ); + + out( + ` + type Extras = { + Int64: typeof binding.Int64; + WeakRef: typeof binding.WeakRef; + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + export function injectNativeModule(nativeModule: any, extras: Extras) { + Object.assign(binding, extras); + `, + ); + + // TODO: Handle injectables + + const injectables = [ + "Long", + "ArrayBuffer", + "Float: utils.Float", + "Status: utils.Status", + "ObjectId", + "UUID", + "Decimal128", + "EJSON_parse: EJSON.parse", + "EJSON_stringify: EJSON.stringify", + "Symbol_for: Symbol.for", + ...spec.classes.map((cls) => cls.jsName), + ]; + + for (const cls of spec.classes) { + const symbolName = `_${cls.rootBase().jsName}_Symbol`; + const bodyLines: string[] = []; + + if (!cls.base) { + // Only root classes get symbols and constructors + out(`const ${symbolName} = Symbol("Realm.${cls.jsName}.external_pointer");`); + bodyLines.push(`${cls.subclasses.length === 0 ? "private" : "protected"} declare [${symbolName}]: unknown;`); + bodyLines.push( + `${cls.subclasses.length === 0 ? "private" : "protected"} constructor(ptr: unknown) { this[${symbolName}] = ptr};`, + ); + } + + bodyLines.push(` + static _extract(self: unknown) { + if (!(self instanceof ${cls.jsName})) + throw new TypeError("Expected a ${cls.jsName}"); + const out = self[${symbolName}]; + if (!out) + throw new TypeError("Received an improperly constructed ${cls.jsName}"); + return out; + }; + `); + + const availableMethods = cls.methods.filter((method) => method.isOptedInTo); + + for (const method of availableMethods) { + // Eagerly bind the name once from the native module to prevent object property lookups on every call + const nativeFreeFunctionName = `_native_${method.id}`; + out(`const ${nativeFreeFunctionName} = nativeModule.${method.id};`); + // TODO consider pre-extracting class-typed arguments while still in JIT VM. + const asyncSig = method.sig.asyncTransform(); + const params = (asyncSig ?? method.sig).args.map((arg) => arg.name); + const args = [method.isStatic ? [] : `this[${symbolName}]`, ...params, asyncSig ? "_cb" : []].flat(); + let call = `${nativeFreeFunctionName}(${args})`; + if (asyncSig) { + // JS can't distinguish between a `const EJson*` that is nullptr (which can't happen), and + // one that points to the string "null" because both become null by the time they reach JS. + // In order to allow the latter (which does happen! E.g. the promise from `response.text()` + // can resolve to `"null"`) we need a special case here. + // TODO see if there is a better approach. + assert(asyncSig.ret.isTemplate("AsyncResult")); + const ret = asyncSig.ret.args[0]; + const nullAllowed = !!(ret.is("Pointer") && ret.type.kind == "Const" && ret.type.type.isPrimitive("EJson")); + call = `_promisify(${nullAllowed}, _cb => ${call})`; + } + bodyLines.push( + method.isStatic ? "static" : "", + method instanceof Property ? "get" : "", + `${method.jsName}(${params.map((name) => name + ": unknown")}) { return ${call}; }`, + ); + } + + if (cls.iterable) { + const native = `_native_${cls.iteratorMethodId()}`; + out(`const ${native} = nativeModule.${cls.iteratorMethodId()};`); + bodyLines.push(`[Symbol.iterator]() { return ${native}(this[${symbolName}]); }`); + } + + out.lines(`class ${cls.jsName} ${cls.base ? `extends ${cls.base.jsName}` : ""} {`, ...bodyLines, `}`); + } + + out(` + Object.defineProperties(binding, { + ${spec.classes.map((cls) => `${cls.jsName}: { value: ${cls.jsName}, writable: false, configurable: false }`)} + }); + `); + + out(`nativeModule.injectInjectables({ ${injectables} });`); + + out("applyPatch(binding); isReady = true; resolveReadyPromise(); }"); +} diff --git a/packages/realm/package.json b/packages/realm/package.json index f33ee4d144..cedf4cadb2 100644 --- a/packages/realm/package.json +++ b/packages/realm/package.json @@ -57,12 +57,6 @@ "scripts", "dist", "!dist/scripts/build/", - "binding/index.js", - "binding/index.d.ts", - "binding/dist", - "binding/generated/*.cjs", - "binding/generated/*.d.cts", - "binding/generated/*.d.ts", "binding/android", "binding/apple", "binding/jsi", @@ -89,6 +83,7 @@ "build:ts": "wireit", "build:node": "wireit", "bindgen:jsi": "wireit", + "bindgen:wrapper": "wireit", "check-types": "wireit", "check-circular-imports": "wireit", "install": "prebuild-install --runtime napi || echo 'Failed to download prebuild for Realm'", @@ -101,10 +96,7 @@ "dependencies": [ "../fetch:build", "build:node", - "bindgen:build:typescript", - "bindgen:generate:node-wrapper", - "bindgen:generate:react-native-wrapper", - "bindgen:transpile" + "bindgen:wrapper" ], "env": { "TSX_TSCONFIG_PATH": "tsconfig.tests.json" @@ -144,23 +136,7 @@ "command": "tsc --build", "dependencies": [ "../fetch:build", - "bindgen:generate:typescript", - "bindgen:generate:node-wrapper", - "bindgen:generate:react-native-wrapper", - "bindgen:transpile" - ] - }, - "bindgen:transpile": { - "command": "babel binding/generated --out-dir binding/generated --extensions .mjs --out-file-extension .cjs --plugins @babel/plugin-transform-modules-commonjs", - "dependencies": [ - "bindgen:generate:node-wrapper", - "bindgen:generate:react-native-wrapper" - ], - "files": [ - "binding/generated/*.mjs" - ], - "output": [ - "binding/generated/*.cjs" + "bindgen:wrapper" ] }, "bindgen:configure": { @@ -172,19 +148,6 @@ "bindgen:configure" ] }, - "bindgen:build:typescript": { - "command": "tsc --project tsconfig.binding.json", - "dependencies": [ - "bindgen:generate:typescript" - ], - "files": [ - "binding/generated/core.ts" - ], - "output": [ - "binding/dist/core.js", - "binding/dist/core.d.ts" - ] - }, "bindgen:jsi": { "command": "realm-bindgen --template bindgen/src/templates/jsi.ts --spec bindgen/vendor/realm-core/bindgen/spec.yml --spec bindgen/js_spec.yml --opt-in bindgen/js_opt_in_spec.yml --output ./binding/jsi", "dependencies": [ @@ -203,48 +166,8 @@ "binding/jsi/jsi_init.cpp" ] }, - "bindgen:generate:typescript": { - "command": "realm-bindgen --template bindgen/src/templates/typescript.ts --spec bindgen/vendor/realm-core/bindgen/spec.yml --spec bindgen/js_spec.yml --opt-in bindgen/js_opt_in_spec.yml --output binding/generated", - "dependencies": [ - "bindgen:generate:spec-schema" - ], - "files": [ - "bindgen/vendor/realm-core/bindgen/spec.yml", - "bindgen/vendor/realm-core/bindgen/src", - "bindgen/js_spec.yml", - "bindgen/js_opt_in_spec.yml", - "bindgen/src", - "!bindgen/src/templates", - "bindgen/src/templates/typescript.ts" - ], - "output": [ - "binding/generated/core.ts", - "binding/generated/native.d.ts" - ] - }, - "bindgen:generate:node-wrapper": { - "command": "realm-bindgen --template bindgen/src/templates/node-wrapper.ts --spec bindgen/vendor/realm-core/bindgen/spec.yml --spec bindgen/js_spec.yml --opt-in bindgen/js_opt_in_spec.yml --output binding/generated", - "dependencies": [ - "bindgen:generate:spec-schema" - ], - "files": [ - "bindgen/vendor/realm-core/bindgen/spec.yml", - "bindgen/vendor/realm-core/bindgen/src", - "bindgen/js_spec.yml", - "bindgen/js_opt_in_spec.yml", - "bindgen/src", - "!bindgen/src/templates", - "bindgen/src/templates/base-wrapper.ts", - "bindgen/src/templates/node-wrapper.ts" - ], - "output": [ - "binding/generated/native.node.mjs", - "binding/generated/native.node.d.mts", - "binding/generated/native.node.d.cts" - ] - }, - "bindgen:generate:react-native-wrapper": { - "command": "realm-bindgen --template bindgen/src/templates/react-native-wrapper.ts --spec bindgen/vendor/realm-core/bindgen/spec.yml --spec bindgen/js_spec.yml --opt-in bindgen/js_opt_in_spec.yml --output binding/generated", + "bindgen:wrapper": { + "command": "realm-bindgen --template bindgen/src/templates/wrapper.ts --spec bindgen/vendor/realm-core/bindgen/spec.yml --spec bindgen/js_spec.yml --opt-in bindgen/js_opt_in_spec.yml --output ./src/binding", "dependencies": [ "bindgen:generate:spec-schema" ], @@ -255,13 +178,10 @@ "bindgen/js_opt_in_spec.yml", "bindgen/src", "!bindgen/src/templates", - "bindgen/src/templates/base-wrapper.ts", - "bindgen/src/templates/react-native-wrapper.ts" + "bindgen/src/templates/wrapper.ts" ], "output": [ - "binding/generated/native.react-native.mjs", - "binding/generated/native.react-native.d.mts", - "binding/generated/native.react-native.d.cts" + "src/binding/wrapper.generated.ts" ] }, "bindgen:generate:spec-schema": { diff --git a/packages/realm/src/ClassHelpers.ts b/packages/realm/src/ClassHelpers.ts index 6bc8dc6af1..bdd5857529 100644 --- a/packages/realm/src/ClassHelpers.ts +++ b/packages/realm/src/ClassHelpers.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import type { binding } from "../binding"; +import type { binding } from "./binding"; import type { CanonicalObjectSchema, DefaultObject, RealmObjectConstructor } from "./schema"; import type { PropertyMap } from "./PropertyMap"; import type { RealmObject } from "./Object"; diff --git a/packages/realm/src/ClassMap.ts b/packages/realm/src/ClassMap.ts index 95679c2c66..b75c6274a5 100644 --- a/packages/realm/src/ClassMap.ts +++ b/packages/realm/src/ClassMap.ts @@ -17,7 +17,7 @@ //////////////////////////////////////////////////////////////////////////// import type { CanonicalObjectSchema, Constructor, RealmObjectConstructor } from "./schema"; -import type { binding } from "../binding"; +import type { binding } from "./binding"; import { PropertyMap } from "./PropertyMap"; import { KEY_ARRAY, KEY_SET, RealmObject } from "./Object"; import { assert } from "./assert"; diff --git a/packages/realm/src/Collection.ts b/packages/realm/src/Collection.ts index 8e36cd6001..191e6a5d61 100644 --- a/packages/realm/src/Collection.ts +++ b/packages/realm/src/Collection.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import type { binding } from "../binding"; +import type { binding } from "./binding"; import { injectIndirect } from "./indirect"; import type { Dictionary } from "./Dictionary"; import type { List } from "./List"; diff --git a/packages/realm/src/Counter.ts b/packages/realm/src/Counter.ts index 8227c3cd05..31b894e08b 100644 --- a/packages/realm/src/Counter.ts +++ b/packages/realm/src/Counter.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../binding"; +import { binding } from "./binding"; import { assert } from "./assert"; import { IllegalConstructorError } from "./errors"; import type { Realm } from "./Realm"; diff --git a/packages/realm/src/Dictionary.ts b/packages/realm/src/Dictionary.ts index 6a5dcf6dd4..8e1ebbfc42 100644 --- a/packages/realm/src/Dictionary.ts +++ b/packages/realm/src/Dictionary.ts @@ -17,7 +17,7 @@ //////////////////////////////////////////////////////////////////////////// import { assert } from "./assert"; -import { binding } from "../binding"; +import { binding } from "./binding"; import { indirect, injectIndirect } from "./indirect"; import { COLLECTION_ACCESSOR as ACCESSOR, Collection, COLLECTION_TYPE_HELPERS as TYPE_HELPERS } from "./Collection"; import { AssertionError, IllegalConstructorError } from "./errors"; diff --git a/packages/realm/src/GeoSpatial.ts b/packages/realm/src/GeoSpatial.ts index 1dbc7a982e..7a293bc8a7 100644 --- a/packages/realm/src/GeoSpatial.ts +++ b/packages/realm/src/GeoSpatial.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../binding"; +import { binding } from "./binding"; /** * Represents the coordinates of a point. The first two required elements of the array are longitude (index 0) and latitude (index 1). diff --git a/packages/realm/src/List.ts b/packages/realm/src/List.ts index 9892a871ad..12fc86eb90 100644 --- a/packages/realm/src/List.ts +++ b/packages/realm/src/List.ts @@ -17,7 +17,7 @@ //////////////////////////////////////////////////////////////////////////// import { assert } from "./assert"; -import { binding } from "../binding"; +import { binding } from "./binding"; import { injectIndirect } from "./indirect"; import { COLLECTION_ACCESSOR as ACCESSOR } from "./Collection"; import { AssertionError, IllegalConstructorError } from "./errors"; diff --git a/packages/realm/src/Logger.ts b/packages/realm/src/Logger.ts index a27ecf45e7..2f8092fd18 100644 --- a/packages/realm/src/Logger.ts +++ b/packages/realm/src/Logger.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../binding"; +import { binding } from "./binding"; import { assert } from "./assert"; import type { App } from "./app-services/App"; diff --git a/packages/realm/src/Object.ts b/packages/realm/src/Object.ts index a93a17d778..b21c75861c 100644 --- a/packages/realm/src/Object.ts +++ b/packages/realm/src/Object.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../binding"; +import { binding } from "./binding"; import { assert } from "./assert"; import { AssertionError, TypeAssertionError } from "./errors"; import { indirect, injectIndirect } from "./indirect"; diff --git a/packages/realm/src/ObjectListeners.ts b/packages/realm/src/ObjectListeners.ts index 25f34e3735..f63482c693 100644 --- a/packages/realm/src/ObjectListeners.ts +++ b/packages/realm/src/ObjectListeners.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../binding"; +import { binding } from "./binding"; import type { RealmObject } from "./Object"; import { Listeners } from "./Listeners"; import type { PropertyMap } from "./PropertyMap"; diff --git a/packages/realm/src/OrderedCollection.ts b/packages/realm/src/OrderedCollection.ts index 9f8cdd842c..475fb3e22b 100644 --- a/packages/realm/src/OrderedCollection.ts +++ b/packages/realm/src/OrderedCollection.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../binding"; +import { binding } from "./binding"; import { assert } from "./assert"; import { IllegalConstructorError, TypeAssertionError } from "./errors"; import { indirect, injectIndirect } from "./indirect"; diff --git a/packages/realm/src/ProgressRealmPromise.ts b/packages/realm/src/ProgressRealmPromise.ts index f8de18938c..708ad62324 100644 --- a/packages/realm/src/ProgressRealmPromise.ts +++ b/packages/realm/src/ProgressRealmPromise.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../binding"; +import { binding } from "./binding"; import { assert } from "./assert"; import { TimeoutError } from "./errors"; import { flags } from "./flags"; diff --git a/packages/realm/src/PropertyHelpers.ts b/packages/realm/src/PropertyHelpers.ts index 4ed1125af3..a8f769fb8a 100644 --- a/packages/realm/src/PropertyHelpers.ts +++ b/packages/realm/src/PropertyHelpers.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../binding"; +import { binding } from "./binding"; import { type TypeOptions, getTypeHelpers, toItemType } from "./TypeHelpers"; import { createArrayPropertyAccessor } from "./property-accessors/Array"; diff --git a/packages/realm/src/PropertyMap.ts b/packages/realm/src/PropertyMap.ts index e547071978..38154fba5c 100644 --- a/packages/realm/src/PropertyMap.ts +++ b/packages/realm/src/PropertyMap.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../binding"; +import { binding } from "./binding"; import { assert } from "./assert"; import type { CanonicalObjectSchema } from "./schema"; import { createPropertyHelpers } from "./PropertyHelpers"; diff --git a/packages/realm/src/Realm.ts b/packages/realm/src/Realm.ts index 536c6c5fcb..748556265c 100644 --- a/packages/realm/src/Realm.ts +++ b/packages/realm/src/Realm.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../binding"; +import { binding } from "./binding"; import { assert } from "./assert"; import { TypeAssertionError } from "./errors"; import { extendDebug } from "./debug"; diff --git a/packages/realm/src/Results.ts b/packages/realm/src/Results.ts index c9228b8f4a..10cd921d9d 100644 --- a/packages/realm/src/Results.ts +++ b/packages/realm/src/Results.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../binding"; +import { binding } from "./binding"; import { assert } from "./assert"; import { IllegalConstructorError } from "./errors"; import { injectIndirect } from "./indirect"; diff --git a/packages/realm/src/Set.ts b/packages/realm/src/Set.ts index 46ec5f3e3b..782ca7a913 100644 --- a/packages/realm/src/Set.ts +++ b/packages/realm/src/Set.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../binding"; +import { binding } from "./binding"; import { assert } from "./assert"; import { IllegalConstructorError } from "./errors"; import { injectIndirect } from "./indirect"; diff --git a/packages/realm/src/TypeHelpers.ts b/packages/realm/src/TypeHelpers.ts index ef5dc64a78..67d9d53b92 100644 --- a/packages/realm/src/TypeHelpers.ts +++ b/packages/realm/src/TypeHelpers.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../binding"; +import { binding } from "./binding"; import { assert } from "./assert"; import { createIntTypeHelpers } from "./type-helpers/Int"; diff --git a/packages/realm/src/app-services/ApiKeyAuth.ts b/packages/realm/src/app-services/ApiKeyAuth.ts index d0acc6e381..dbbd87a3c7 100644 --- a/packages/realm/src/app-services/ApiKeyAuth.ts +++ b/packages/realm/src/app-services/ApiKeyAuth.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import type { binding } from "../../binding"; +import type { binding } from "../binding"; import { BSON } from "../bson"; import { assert } from "../assert"; diff --git a/packages/realm/src/app-services/App.ts b/packages/realm/src/app-services/App.ts index 87a1c58135..79a50b6ef2 100644 --- a/packages/realm/src/app-services/App.ts +++ b/packages/realm/src/app-services/App.ts @@ -18,7 +18,7 @@ import type { AnyFetch } from "@realm/fetch"; -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { injectIndirect } from "../indirect"; import type { BaseConfiguration } from "../Configuration"; diff --git a/packages/realm/src/app-services/BaseSubscriptionSet.ts b/packages/realm/src/app-services/BaseSubscriptionSet.ts index db1e778351..8aaffc8854 100644 --- a/packages/realm/src/app-services/BaseSubscriptionSet.ts +++ b/packages/realm/src/app-services/BaseSubscriptionSet.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { indirect } from "../indirect"; import type { Realm } from "../Realm"; diff --git a/packages/realm/src/app-services/Credentials.ts b/packages/realm/src/app-services/Credentials.ts index 49450be34b..3fff28581c 100644 --- a/packages/realm/src/app-services/Credentials.ts +++ b/packages/realm/src/app-services/Credentials.ts @@ -17,7 +17,7 @@ //////////////////////////////////////////////////////////////////////////// import type { App } from "./App"; -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; /** diff --git a/packages/realm/src/app-services/EmailPasswordAuth.ts b/packages/realm/src/app-services/EmailPasswordAuth.ts index fd8405a411..cdcac35f5f 100644 --- a/packages/realm/src/app-services/EmailPasswordAuth.ts +++ b/packages/realm/src/app-services/EmailPasswordAuth.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import type { binding } from "../../binding"; +import type { binding } from "../binding"; /** * Authentication provider where users identify using email and password. diff --git a/packages/realm/src/app-services/MongoDBCollection.ts b/packages/realm/src/app-services/MongoDBCollection.ts index a3f2ee5245..80e3f3543e 100644 --- a/packages/realm/src/app-services/MongoDBCollection.ts +++ b/packages/realm/src/app-services/MongoDBCollection.ts @@ -18,7 +18,7 @@ import type { Long, Timestamp } from "bson"; -import { binding } from "../../binding"; +import { binding } from "../binding"; import { toArrayBuffer } from "../type-helpers/array-buffer"; import type { User } from "./User"; import { type DefaultFunctionsFactory, createFactory } from "./FunctionsFactory"; diff --git a/packages/realm/src/app-services/MutableSubscriptionSet.ts b/packages/realm/src/app-services/MutableSubscriptionSet.ts index 741fb24158..cee0d2ad35 100644 --- a/packages/realm/src/app-services/MutableSubscriptionSet.ts +++ b/packages/realm/src/app-services/MutableSubscriptionSet.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { indirect } from "../indirect"; import type { Realm } from "../Realm"; diff --git a/packages/realm/src/app-services/NetworkTransport.ts b/packages/realm/src/app-services/NetworkTransport.ts index 4adb58eda8..6230e3c00b 100644 --- a/packages/realm/src/app-services/NetworkTransport.ts +++ b/packages/realm/src/app-services/NetworkTransport.ts @@ -18,7 +18,7 @@ import type { Headers } from "@realm/fetch"; -import { binding } from "../../binding"; +import { binding } from "../binding"; import { extendDebug } from "../debug"; import { network } from "../platform"; diff --git a/packages/realm/src/app-services/PushClient.ts b/packages/realm/src/app-services/PushClient.ts index aa1b7a810b..6d5d2364e4 100644 --- a/packages/realm/src/app-services/PushClient.ts +++ b/packages/realm/src/app-services/PushClient.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import type { binding } from "../../binding"; +import type { binding } from "../binding"; /** * Authentication provider where users identify using an API-key. * @deprecated https://www.mongodb.com/docs/atlas/app-services/reference/push-notifications/ diff --git a/packages/realm/src/app-services/Subscription.ts b/packages/realm/src/app-services/Subscription.ts index 26bb893731..c97a52e284 100644 --- a/packages/realm/src/app-services/Subscription.ts +++ b/packages/realm/src/app-services/Subscription.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import type { binding } from "../../binding"; +import type { binding } from "../binding"; import type { SubscriptionSet } from "./SubscriptionSet"; import type { BSON } from "../bson"; diff --git a/packages/realm/src/app-services/SubscriptionSet.ts b/packages/realm/src/app-services/SubscriptionSet.ts index 6496ced9a6..9df31376bf 100644 --- a/packages/realm/src/app-services/SubscriptionSet.ts +++ b/packages/realm/src/app-services/SubscriptionSet.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import type { Realm } from "../Realm"; import { BaseSubscriptionSet, SubscriptionSetState } from "./BaseSubscriptionSet"; diff --git a/packages/realm/src/app-services/Sync.ts b/packages/realm/src/app-services/Sync.ts index 15b1088672..93660a8bd6 100644 --- a/packages/realm/src/app-services/Sync.ts +++ b/packages/realm/src/app-services/Sync.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { type LogLevel, type Logger, fromBindingLoggerLevelToNumericLogLevel, toBindingLoggerLevel } from "../Logger"; import type { Realm } from "../Realm"; diff --git a/packages/realm/src/app-services/SyncConfiguration.ts b/packages/realm/src/app-services/SyncConfiguration.ts index 3eaac521fd..f68f25d6bb 100644 --- a/packages/realm/src/app-services/SyncConfiguration.ts +++ b/packages/realm/src/app-services/SyncConfiguration.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { BSON } from "../bson"; import type { ClientResetError, SyncError } from "../errors"; diff --git a/packages/realm/src/app-services/SyncSession.ts b/packages/realm/src/app-services/SyncSession.ts index 7155a6441a..22452eafd5 100644 --- a/packages/realm/src/app-services/SyncSession.ts +++ b/packages/realm/src/app-services/SyncSession.ts @@ -18,7 +18,7 @@ import { EJSON } from "bson"; -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { ClientResetError, fromBindingSyncError } from "../errors"; import { indirect } from "../indirect"; @@ -133,7 +133,7 @@ function toBindingProgressNotificationCallback(callback: ProgressNotificationCal } else { // eslint-disable-next-line @typescript-eslint/no-unused-vars return (transferredBytes: binding.Int64, transferrableBytes: binding.Int64, _: number) => - callback(transferredBytes, transferrableBytes); + callback(binding.Int64.intToNum(transferredBytes), binding.Int64.intToNum(transferrableBytes)); } } diff --git a/packages/realm/src/app-services/User.ts b/packages/realm/src/app-services/User.ts index 90204ff8f7..684a6cb871 100644 --- a/packages/realm/src/app-services/User.ts +++ b/packages/realm/src/app-services/User.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { indirect, injectIndirect } from "../indirect"; import { network } from "../platform"; diff --git a/packages/realm/src/app-services/utils.ts b/packages/realm/src/app-services/utils.ts index 629d36e528..31a95464e3 100644 --- a/packages/realm/src/app-services/utils.ts +++ b/packages/realm/src/app-services/utils.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import type { binding } from "../../binding"; +import type { binding } from "../binding"; /** * Remove entries for undefined property values. diff --git a/packages/realm/src/assert.ts b/packages/realm/src/assert.ts index d3d7e50d2b..3cf864205f 100644 --- a/packages/realm/src/assert.ts +++ b/packages/realm/src/assert.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import type { binding } from "../binding"; +import type { binding } from "./binding"; import { AssertionError, TypeAssertionError } from "./errors"; import type { DefaultObject } from "./schema"; import type { Realm } from "./Realm"; diff --git a/packages/realm/src/binding/NativeBigInt.ts b/packages/realm/src/binding/NativeBigInt.ts new file mode 100644 index 0000000000..2dd8fe85e7 --- /dev/null +++ b/packages/realm/src/binding/NativeBigInt.ts @@ -0,0 +1,50 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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 { assert } from "../assert"; +import type { binding } from "./wrapper.generated"; + +export class NativeBigInt { + static add(a: binding.Int64, b: binding.Int64): binding.Int64 { + assert(typeof a === "bigint"); + assert(typeof b === "bigint"); + return (a + b) as unknown as binding.Int64; + } + + static equals(a: binding.Int64, b: binding.Int64 | number | string) { + assert(typeof a === "bigint"); + assert(typeof b === "bigint" || typeof b === "number" || typeof b === "string"); + return a == b; // using == rather than === to support number and string RHS! + } + + static isInt(a: unknown): a is binding.Int64 { + return typeof a === "bigint"; + } + + static numToInt(a: number) { + return BigInt(a) as unknown as binding.Int64; + } + + static strToInt(a: string) { + return BigInt(a) as unknown as binding.Int64; + } + + static intToNum(a: binding.Int64) { + return Number(a); + } +} diff --git a/packages/realm/src/binding/PolyfilledBigInt.ts b/packages/realm/src/binding/PolyfilledBigInt.ts new file mode 100644 index 0000000000..d4f965feea --- /dev/null +++ b/packages/realm/src/binding/PolyfilledBigInt.ts @@ -0,0 +1,56 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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 { Long } from "bson"; + +import type { binding } from "./wrapper.generated"; +import { assert } from "../assert"; + +export class PolyfilledBigInt { + static add(a: binding.Int64, b: binding.Int64) { + assert.instanceOf(a, Long); + assert.instanceOf(b, Long); + return a.add(b) as unknown as binding.Int64; + } + + static equals(a: binding.Int64, b: binding.Int64 | number | string) { + assert.instanceOf(a, Long); + assert( + typeof b === "number" || typeof b === "string" || (typeof b === "object" && b instanceof Long), + "Expected a 'BSON.Long', or number, or string.", + ); + return a.equals(b); + } + + static isInt(a: unknown): a is binding.Int64 { + return a instanceof Long; + } + + static numToInt(a: number) { + return Long.fromNumber(a) as unknown as binding.Int64; + } + + static strToInt(a: string) { + return Long.fromString(a) as unknown as binding.Int64; + } + + static intToNum(a: binding.Int64) { + assert.instanceOf(a, Long); + return a.toNumber(); + } +} diff --git a/packages/realm/src/binding/index.ts b/packages/realm/src/binding/index.ts new file mode 100644 index 0000000000..cb22a32ee6 --- /dev/null +++ b/packages/realm/src/binding/index.ts @@ -0,0 +1,19 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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. +// +//////////////////////////////////////////////////////////////////////////// + +export { binding } from "./wrapper.generated"; diff --git a/packages/realm/src/platform/binding-patch.ts b/packages/realm/src/binding/patch.ts similarity index 70% rename from packages/realm/src/platform/binding-patch.ts rename to packages/realm/src/binding/patch.ts index 2009a5c298..68ee8dd03b 100644 --- a/packages/realm/src/platform/binding-patch.ts +++ b/packages/realm/src/binding/patch.ts @@ -20,44 +20,46 @@ import type { fetch } from "@realm/fetch"; import { AbortSignal } from "@realm/fetch"; /** @internal */ -import type * as binding from "../../binding/generated/native"; - +import type { binding } from "./wrapper.generated"; type Binding = typeof binding; /** @internal */ -declare module "../../binding/generated/native" { - /** @internal */ - export interface IndexSet { - asIndexes(): Iterator; - } - export interface Timestamp { - toDate(): Date; - } +declare module "./wrapper.generated" { // eslint-disable-next-line @typescript-eslint/no-namespace - export namespace Timestamp { - function fromDate(d: Date): binding.Timestamp; - } - export interface SyncSession { - /** Returns a WeakSyncSession and releases the strong reference held by this SyncSession */ - weaken(): WeakSyncSession; - } + namespace binding { + /** @internal */ + export interface IndexSet { + asIndexes(): Iterator; + } + export interface Timestamp { + toDate(): Date; + } + // eslint-disable-next-line @typescript-eslint/no-namespace + export namespace Timestamp { + function fromDate(d: Date): binding.Timestamp; + } + export interface SyncSession { + /** Returns a WeakSyncSession and releases the strong reference held by this SyncSession */ + weaken(): WeakSyncSession; + } - export interface WeakSyncSession { - /** - * Similar to WeakRef.deref(), but takes a callback so that the strong reference can be - * automatically released when the callback exists (either by returning or throwing). - * It is not legal to hold on to the SyncSession after this returns because its - * strong reference will have been deleted. - */ - withDeref(callback: (shared: SyncSession | null) => Ret): Ret; - } + export interface WeakSyncSession { + /** + * Similar to WeakRef.deref(), but takes a callback so that the strong reference can be + * automatically released when the callback exists (either by returning or throwing). + * It is not legal to hold on to the SyncSession after this returns because its + * strong reference will have been deleted. + */ + withDeref(callback: (shared: SyncSession | null) => Ret): Ret; + } - export class InvalidObjKey extends TypeError { - constructor(input: string); + export class InvalidObjKey extends TypeError { + constructor(input: string); + } + export function stringToObjKey(input: string): binding.ObjKey; + export function isEmptyObjKey(objKey: binding.ObjKey): boolean; + export function toFetchArgs(request: binding.Request): Parameters; } - export function stringToObjKey(input: string): binding.ObjKey; - export function isEmptyObjKey(objKey: binding.ObjKey): boolean; - export function toFetchArgs(request: binding.Request): Parameters; } /** @@ -145,8 +147,8 @@ export function applyPatch(binding: Binding) { } } - function fromBindingTimeoutSignal(timeoutMs: binding.Int64Type): AbortSignal | undefined { - const timeout = Number(timeoutMs); + function fromBindingTimeoutSignal(timeoutMs: binding.Int64): AbortSignal | undefined { + const timeout = binding.Int64.intToNum(timeoutMs); return timeout > 0 ? AbortSignal.timeout(timeout) : undefined; } diff --git a/packages/realm/src/binding/utils.ts b/packages/realm/src/binding/utils.ts new file mode 100644 index 0000000000..8d6fa52adb --- /dev/null +++ b/packages/realm/src/binding/utils.ts @@ -0,0 +1,77 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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. +// +//////////////////////////////////////////////////////////////////////////// + +// Copied from lib/utils.js of the v11 SDK +// It might be slightly faster to make dedicated wrapper for 1 and 2 argument forms, but unlikely to be worth it. + +/** + * Calls a function with a completion callback and turns it into a Promise. + * @internal + */ +export function _promisify(nullAllowed: boolean, func: (cb: (...args: Args) => void) => void) { + return new Promise((resolve, reject) => { + func((...args: Args) => { + // Any errors in this function should flow into the Promise chain, rather than out to the caller, + // since callers of async callbacks aren't expecting exceptions. + try { + if (args.length < 1 || args.length > 2) throw Error("invalid args length " + args.length); + // The last argument is always an error + const error = args[args.length - 1]; + if (error) { + reject(error); + } else if (args.length === 2) { + const result = args[0]; + resolve(result); + } else { + resolve(undefined); + } + } catch (err) { + reject(err); + } + }); + }); +} + +/** + * Throws an error when a property is accessed before the native module has been injected. + * @internal + */ +export function _throwOnAccess(propertyName: string) { + throw new Error(`Accessed property '${propertyName} before the native module was injected into the Realm binding'`); +} + +// Wrapped types + +export class Float { + constructor(public value: number) {} + valueOf() { + return this.value; + } +} + +export class Status { + public isOk: boolean; + public code?: number; + public reason?: string; + constructor(isOk: boolean) { + this.isOk = isOk; + } +} + +export const ListSentinel = Symbol.for("Realm.List"); +export const DictionarySentinel = Symbol.for("Realm.Dictionary"); diff --git a/packages/realm/src/collection-accessors/Dictionary.ts b/packages/realm/src/collection-accessors/Dictionary.ts index b2fb9b02d2..94a1e16883 100644 --- a/packages/realm/src/collection-accessors/Dictionary.ts +++ b/packages/realm/src/collection-accessors/Dictionary.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { indirect } from "../indirect"; import type { Dictionary } from "../Dictionary"; diff --git a/packages/realm/src/collection-accessors/List.ts b/packages/realm/src/collection-accessors/List.ts index 801e43c632..795df8b6dc 100644 --- a/packages/realm/src/collection-accessors/List.ts +++ b/packages/realm/src/collection-accessors/List.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { indirect } from "../indirect"; import { createDictionaryAccessor, insertIntoDictionaryOfMixed, isJsOrRealmDictionary } from "./Dictionary"; diff --git a/packages/realm/src/collection-accessors/OrderedCollection.ts b/packages/realm/src/collection-accessors/OrderedCollection.ts index 39f079e001..08e2fab548 100644 --- a/packages/realm/src/collection-accessors/OrderedCollection.ts +++ b/packages/realm/src/collection-accessors/OrderedCollection.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import type { OrderedCollectionInternal } from "../OrderedCollection"; import type { TypeHelpers } from "../TypeHelpers"; diff --git a/packages/realm/src/collection-accessors/Results.ts b/packages/realm/src/collection-accessors/Results.ts index a417cdd030..d2002239fe 100644 --- a/packages/realm/src/collection-accessors/Results.ts +++ b/packages/realm/src/collection-accessors/Results.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { indirect } from "../indirect"; import { createDictionaryAccessor } from "./Dictionary"; import { createListAccessor } from "./List"; diff --git a/packages/realm/src/collection-accessors/Set.ts b/packages/realm/src/collection-accessors/Set.ts index 3dfacddef7..a95f1d6cb5 100644 --- a/packages/realm/src/collection-accessors/Set.ts +++ b/packages/realm/src/collection-accessors/Set.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import type { TypeHelpers } from "../TypeHelpers"; import type { Realm } from "../Realm"; diff --git a/packages/realm/src/errors.ts b/packages/realm/src/errors.ts index 4e4f8d24d9..7e3963c518 100644 --- a/packages/realm/src/errors.ts +++ b/packages/realm/src/errors.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import type { binding } from "../binding"; +import type { binding } from "./binding"; import type { PrimaryKey } from "./schema"; import type { Configuration } from "./Configuration"; import type { ClientResetMode } from "./app-services/SyncConfiguration"; diff --git a/packages/realm/src/platform/binding.ts b/packages/realm/src/platform/binding.ts index 6d267741fb..9aabe68aaf 100644 --- a/packages/realm/src/platform/binding.ts +++ b/packages/realm/src/platform/binding.ts @@ -17,14 +17,8 @@ //////////////////////////////////////////////////////////////////////////// /** @internal */ -import { binding, inject } from "../../binding"; -import { applyPatch } from "./binding-patch"; - +export { binding, injectNativeModule } from "../binding/wrapper.generated"; /** @internal */ -export { binding }; - +export { NativeBigInt } from "../binding/NativeBigInt"; /** @internal */ -export function injectAndPatch(value: typeof binding) { - applyPatch(value); - inject(value); -} +export { PolyfilledBigInt } from "../binding/PolyfilledBigInt"; diff --git a/packages/realm/src/platform/node/binding.ts b/packages/realm/src/platform/node/binding.ts index 9bbdc6a879..c89d7d6f29 100644 --- a/packages/realm/src/platform/node/binding.ts +++ b/packages/realm/src/platform/node/binding.ts @@ -16,7 +16,8 @@ // //////////////////////////////////////////////////////////////////////////// -import { injectAndPatch } from "../binding"; -import * as binding from "../../../binding/generated/native.node.cjs"; +import { NativeBigInt, type binding, injectNativeModule } from "../binding"; -injectAndPatch(binding); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const nativeModule = require("#realm.node"); +injectNativeModule(nativeModule, { Int64: NativeBigInt as typeof binding.Int64, WeakRef }); diff --git a/packages/realm/src/platform/react-native/binding.ts b/packages/realm/src/platform/react-native/binding.ts index 4d02fdd9ed..e5199590af 100644 --- a/packages/realm/src/platform/react-native/binding.ts +++ b/packages/realm/src/platform/react-native/binding.ts @@ -16,7 +16,37 @@ // //////////////////////////////////////////////////////////////////////////// -import { injectAndPatch } from "../binding"; -import * as binding from "../../../binding/generated/native.react-native.cjs"; +declare const global: Record; -injectAndPatch(binding); +import { NativeModules } from "react-native"; +import { NativeBigInt, PolyfilledBigInt, type binding, injectNativeModule } from "../binding"; +import { assert } from "../../assert"; + +try { + const RealmNativeModule = NativeModules.Realm; + RealmNativeModule.injectModuleIntoJSGlobal(); + // Read the global into the local scope + const { __injectedRealmBinding: nativeModule } = global; + // Delete the global again + delete global.__injectedRealmBinding; + // Inject the native module into the binding + assert.object(nativeModule, "nativeModule"); + injectNativeModule(nativeModule, { + Int64: (global.HermesInternal ? NativeBigInt : PolyfilledBigInt) as typeof binding.Int64, + WeakRef: class WeakRef { + private native: unknown; + constructor(obj: unknown) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- See "createWeakRef" protocol in the jsi bindgen template + this.native = (nativeModule as any).createWeakRef(obj); + } + deref() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- See "createWeakRef" protocol in the jsi bindgen template + return (nativeModule as any).lockWeakRef(this.native); + } + }, + }); +} catch (err) { + throw new Error( + "Could not find the Realm binary. Please consult our troubleshooting guide: https://www.mongodb.com/docs/realm-sdks/js/latest/#md:troubleshooting-missing-binary", + ); +} diff --git a/packages/realm/src/property-accessors/Array.ts b/packages/realm/src/property-accessors/Array.ts index 2f687c2f8b..d42e02698a 100644 --- a/packages/realm/src/property-accessors/Array.ts +++ b/packages/realm/src/property-accessors/Array.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { createListAccessor } from "../collection-accessors/List"; import { createResultsAccessor } from "../collection-accessors/Results"; diff --git a/packages/realm/src/property-accessors/Dictionary.ts b/packages/realm/src/property-accessors/Dictionary.ts index 64f0819b39..16a4e68de0 100644 --- a/packages/realm/src/property-accessors/Dictionary.ts +++ b/packages/realm/src/property-accessors/Dictionary.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { TypeAssertionError } from "../errors"; import { Dictionary } from "../Dictionary"; diff --git a/packages/realm/src/property-accessors/Mixed.ts b/packages/realm/src/property-accessors/Mixed.ts index e2155305e8..ad3b470e9b 100644 --- a/packages/realm/src/property-accessors/Mixed.ts +++ b/packages/realm/src/property-accessors/Mixed.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { Dictionary } from "../Dictionary"; import { diff --git a/packages/realm/src/property-accessors/Object.ts b/packages/realm/src/property-accessors/Object.ts index a171dc0d66..26b1e1e77e 100644 --- a/packages/realm/src/property-accessors/Object.ts +++ b/packages/realm/src/property-accessors/Object.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import type { binding } from "../../binding"; +import type { binding } from "../binding"; import { assert } from "../assert"; 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 940c53f048..6cc2f8592c 100644 --- a/packages/realm/src/property-accessors/Set.ts +++ b/packages/realm/src/property-accessors/Set.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { RealmSet } from "../Set"; import type { PropertyAccessor, PropertyOptions } from "./types"; diff --git a/packages/realm/src/property-accessors/default.ts b/packages/realm/src/property-accessors/default.ts index 6bd293accf..73bf1b1d7c 100644 --- a/packages/realm/src/property-accessors/default.ts +++ b/packages/realm/src/property-accessors/default.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import type { binding } from "../../binding"; +import type { binding } from "../binding"; import { assert } from "../assert"; import type { PropertyAccessor, PropertyOptions } from "./types"; diff --git a/packages/realm/src/property-accessors/types.ts b/packages/realm/src/property-accessors/types.ts index 05c6194f0d..6cfa601670 100644 --- a/packages/realm/src/property-accessors/types.ts +++ b/packages/realm/src/property-accessors/types.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import type { binding } from "../../binding"; +import type { binding } from "../binding"; import type { ClassHelpers } from "../ClassHelpers"; import type { ListAccessor } from "../collection-accessors/List"; import type { Realm } from "../Realm"; diff --git a/packages/realm/src/schema/from-binding.ts b/packages/realm/src/schema/from-binding.ts index 6d260f58f4..2e4169da5c 100644 --- a/packages/realm/src/schema/from-binding.ts +++ b/packages/realm/src/schema/from-binding.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; type BindingObjectSchema = binding.ObjectSchema; diff --git a/packages/realm/src/schema/to-binding.ts b/packages/realm/src/schema/to-binding.ts index 0b4fda5e22..ada2335c19 100644 --- a/packages/realm/src/schema/to-binding.ts +++ b/packages/realm/src/schema/to-binding.ts @@ -17,7 +17,7 @@ //////////////////////////////////////////////////////////////////////////// import { assert } from "../assert"; -import { binding } from "../../binding"; +import { binding } from "../binding"; import type { CanonicalObjectSchema, CanonicalPropertySchema, PropertyTypeName } from "../schema"; type BindingObjectSchema = binding.ObjectSchema_Relaxed; diff --git a/packages/realm/src/type-helpers/Array.ts b/packages/realm/src/type-helpers/Array.ts index cd3d386d42..0bb73e3748 100644 --- a/packages/realm/src/type-helpers/Array.ts +++ b/packages/realm/src/type-helpers/Array.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { indirect } from "../indirect"; import type { TypeHelpers, TypeOptions } from "./types"; diff --git a/packages/realm/src/type-helpers/Date.ts b/packages/realm/src/type-helpers/Date.ts index 2a5e972997..4c1aa6064b 100644 --- a/packages/realm/src/type-helpers/Date.ts +++ b/packages/realm/src/type-helpers/Date.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { nullPassthrough } from "./null-passthrough"; import type { TypeOptions } from "./types"; diff --git a/packages/realm/src/type-helpers/Float.ts b/packages/realm/src/type-helpers/Float.ts index ebcaac14a0..0fb11dc7f0 100644 --- a/packages/realm/src/type-helpers/Float.ts +++ b/packages/realm/src/type-helpers/Float.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { nullPassthrough } from "./null-passthrough"; import type { TypeHelpers, TypeOptions } from "./types"; diff --git a/packages/realm/src/type-helpers/Int.ts b/packages/realm/src/type-helpers/Int.ts index 757aebf1ba..2c403176dc 100644 --- a/packages/realm/src/type-helpers/Int.ts +++ b/packages/realm/src/type-helpers/Int.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { TypeAssertionError } from "../errors"; import { Counter } from "../Counter"; import { nullPassthrough } from "./null-passthrough"; diff --git a/packages/realm/src/type-helpers/LinkingObjects.ts b/packages/realm/src/type-helpers/LinkingObjects.ts index f39035f16c..2cfb49b5b0 100644 --- a/packages/realm/src/type-helpers/LinkingObjects.ts +++ b/packages/realm/src/type-helpers/LinkingObjects.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { defaultToBinding } from "./default"; import type { TypeHelpers, TypeOptions } from "./types"; diff --git a/packages/realm/src/type-helpers/Mixed.ts b/packages/realm/src/type-helpers/Mixed.ts index 7d46072e6d..2437747e3a 100644 --- a/packages/realm/src/type-helpers/Mixed.ts +++ b/packages/realm/src/type-helpers/Mixed.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { indirect } from "../indirect"; import { createDictionaryAccessor } from "../collection-accessors/Dictionary"; diff --git a/packages/realm/src/type-helpers/Object.ts b/packages/realm/src/type-helpers/Object.ts index b975ec2e5d..18405be21b 100644 --- a/packages/realm/src/type-helpers/Object.ts +++ b/packages/realm/src/type-helpers/Object.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { RealmObject, UpdateMode } from "../Object"; import { OBJECT_INTERNAL, OBJECT_REALM } from "../symbols"; diff --git a/packages/realm/src/type-helpers/array-buffer.ts b/packages/realm/src/type-helpers/array-buffer.ts index 254a81aca5..66dbd403e3 100644 --- a/packages/realm/src/type-helpers/array-buffer.ts +++ b/packages/realm/src/type-helpers/array-buffer.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { binding } from "../../binding"; +import { binding } from "../binding"; import { assert } from "../assert"; import { safeGlobalThis } from "../safeGlobalThis"; diff --git a/packages/realm/src/type-helpers/default.ts b/packages/realm/src/type-helpers/default.ts index a6e10b2958..28150cb969 100644 --- a/packages/realm/src/type-helpers/default.ts +++ b/packages/realm/src/type-helpers/default.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import type { binding } from "../../binding"; +import type { binding } from "../binding"; /** @internal */ export function defaultToBinding(value: unknown): binding.MixedArg { diff --git a/packages/realm/src/type-helpers/types.ts b/packages/realm/src/type-helpers/types.ts index bf27eaeeb4..55b2674f58 100644 --- a/packages/realm/src/type-helpers/types.ts +++ b/packages/realm/src/type-helpers/types.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import type { binding } from "../../binding"; +import type { binding } from "../binding"; import type { ClassHelpers } from "../ClassHelpers"; import type { PresentationPropertyTypeName } from "../schema"; import type { ObjCreator, UpdateMode } from "../Object"; diff --git a/packages/realm/tsconfig.binding.json b/packages/realm/tsconfig.binding.json deleted file mode 100644 index e0d5918652..0000000000 --- a/packages/realm/tsconfig.binding.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "outDir": "binding/dist", - "rootDir": "binding/generated", - "target": "es2022", - "module": "Node16", - "moduleResolution": "Node16", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "preserveConstEnums": true, - "lib": [ - "ES2022" - ], - "types": [ - "@realm/fetch" - ], - "noResolve": true - }, - "files": [ - "./binding/generated/core.ts" - ] -} \ No newline at end of file diff --git a/packages/realm/tsconfig.docs.json b/packages/realm/tsconfig.docs.json index 2ef96bcac0..e6a31c2e27 100644 --- a/packages/realm/tsconfig.docs.json +++ b/packages/realm/tsconfig.docs.json @@ -12,8 +12,5 @@ }, "include": [ "dist/public-types" - ], - "exclude": [ - "./binding/generated/native.d.ts" ] } \ No newline at end of file diff --git a/packages/realm/tsconfig.shared.json b/packages/realm/tsconfig.shared.json index a54a1902e6..a313e4ed82 100644 --- a/packages/realm/tsconfig.shared.json +++ b/packages/realm/tsconfig.shared.json @@ -18,10 +18,7 @@ "types": [ "bson", "debug", - "@realm/fetch", - "./binding/generated/native.d.ts", - "./binding/dist/core.d.ts", - "./binding/index.d.ts" + "@realm/fetch" ] }, "include": [ @@ -32,8 +29,5 @@ "src/scripts", "src/platform/node", "src/platform/react-native" - ], - "references": [ - { "path": "./tsconfig.binding.json" } ] } \ No newline at end of file