diff --git a/skiplang/prelude/src/skstore/Context.sk b/skiplang/prelude/src/skstore/Context.sk index d90e6e908..2fe5b21b4 100644 --- a/skiplang/prelude/src/skstore/Context.sk +++ b/skiplang/prelude/src/skstore/Context.sk @@ -2303,11 +2303,18 @@ class ConcurrentFork() extends .Exception { } } -fun mergeForkNoGc( +class MergeState( + settings: ForkSettings, + start: Tick, + contexts: ContextsWithActions, + fork: ?String, +) + +fun startMergeForkNoGc( name: String, settings: ForkSettings = ForkSettings::keepall(), synchronizer: ?ForkSynchronizer = None(), -): void { +): MergeState { SKStore.gGlobalLock(); contexts = SKStore.gContextsGetNoLock(); if (!contexts.has(Some(name))) { @@ -2340,11 +2347,7 @@ fun mergeForkNoGc( new = contexts.check(fork.fork, new_context.clone(), fork.postponables); !new.contexts = new.contexts.remove(name); SKStore.gUnsafeFree(contexts); - sync = UInt32::truncate(if (settings.isSync) 1 else 0); - SKStore.gContextsReplaceNoLock(new.contexts, sync); - SKStore.gGlobalUnlock(); - new.contexts.get(fork.fork).notifyAll(start, settings.ignoredSessions); - new.actions.each(fn -> fn()) + MergeState(settings, start, new, fork.fork) | Failure(ex) -> SKStore.gUnsafeFree(contexts); SKStore.gGlobalUnlock(); @@ -2352,6 +2355,30 @@ fun mergeForkNoGc( } } +fun endMergeForkNoGc(state: MergeState): void { + new = state.contexts; + sync = UInt32::truncate(if (state.settings.isSync) 1 else 0); + SKStore.gContextsReplaceNoLock(new.contexts, sync); + SKStore.gGlobalUnlock(); + new.contexts + .get(state.fork) + .notifyAll(state.start, state.settings.ignoredSessions); + new.actions.each(fn -> fn()) +} + +fun abortMergeForkNoGc(_state: MergeState): void { + // The unused parameter is here the be call in merge context + SKStore.gGlobalUnlock(); +} + +fun mergeForkNoGc( + name: String, + settings: ForkSettings = ForkSettings::keepall(), + synchronizer: ?ForkSynchronizer = None(), +): void { + endMergeForkNoGc(startMergeForkNoGc(name, settings, synchronizer)) +} + fun removeForkNoGc(name: String, isSync: Bool = false): void { gGlobalLock(); contexts = gContextsGetNoLock(); diff --git a/skiplang/prelude/ts/wasm/src/sk_types.ts b/skiplang/prelude/ts/wasm/src/sk_types.ts index dcc4d3793..2de72c2f3 100644 --- a/skiplang/prelude/ts/wasm/src/sk_types.ts +++ b/skiplang/prelude/ts/wasm/src/sk_types.ts @@ -668,6 +668,25 @@ export class Utils { } }; + async unsafeAsyncWithGc(fn: () => Promise): Promise { + this.stddebug = []; + const obsPos = this.exports.SKIP_new_Obstack(); + try { + const value = await fn(); + // clone must be done before SKIP_destroy_Obstack + const res = cloneIfProxy(value); + this.exports.SKIP_destroy_Obstack(obsPos); + return res; + } catch (ex) { + this.exports.SKIP_destroy_Obstack(obsPos); + throw ex; + } finally { + if (this.stddebug.length > 0) { + console.log(this.stddebug.join("")); + } + } + } + getState(name: string, create: () => T): T { let state = this.states.get(name); if (state == undefined) { diff --git a/skipruntime-ts/addon/src/common.cc b/skipruntime-ts/addon/src/common.cc index 853b53d9e..819e1e97f 100644 --- a/skipruntime-ts/addon/src/common.cc +++ b/skipruntime-ts/addon/src/common.cc @@ -389,6 +389,109 @@ void RunWithGC(const v8::FunctionCallbackInfo& args) { SKIP_destroy_Obstack(obstack); } +void UnsafeAsyncRunWithGC(const v8::FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + HandleScope scope(isolate); + if (args.Length() != 1) { + isolate->ThrowException( + Exception::TypeError(FromUtf8(isolate, "Must have one parameters."))); + return; + }; + if (!args[0]->IsFunction()) { + isolate->ThrowException(Exception::TypeError( + FromUtf8(isolate, "The parameter must be a function."))); + return; + } + SKObstack obstack = SKIP_new_Obstack(); + Local context = isolate->GetCurrentContext(); + TryCatch tryCatch(isolate); + MaybeLocal optResult = + args[0].As()->Call(context, Null(isolate), 0, nullptr); + + if (tryCatch.HasCaught()) { + SKIP_destroy_Obstack(obstack); + tryCatch.ReThrow(); + return; + } + + Local result = optResult.ToLocalChecked(); + + if (result->IsPromise()) { + Local promise = result.As(); + + Local obstackData = External::New(isolate, obstack); + + Local resolver = + v8::Promise::Resolver::New(context).ToLocalChecked(); + Local wrapperPromise = resolver->GetPromise(); + + // Create handler data array with obstack and resolver + Local handlerData = Array::New(isolate, 2); + handlerData->Set(context, 0, obstackData).ToChecked(); + handlerData->Set(context, 1, resolver).ToChecked(); + + // Create success handler that cleans up and resolves + Local onFulfilled = + Function::New( + context, + [](const v8::FunctionCallbackInfo& info) { + Isolate* isolate = info.GetIsolate(); + HandleScope scope(isolate); + Local context = isolate->GetCurrentContext(); + + Local data = info.Data().As(); + SKObstack obstack = data->Get(context, 0) + .ToLocalChecked() + .As() + ->Value(); + Local resolver = + data->Get(context, 1) + .ToLocalChecked() + .As(); + + SKIP_destroy_Obstack(obstack); + + Local value = info[0]; + resolver->Resolve(context, value).ToChecked(); + }, + handlerData) + .ToLocalChecked(); + + Local onRejected = + Function::New( + context, + [](const v8::FunctionCallbackInfo& info) { + Isolate* isolate = info.GetIsolate(); + HandleScope scope(isolate); + Local context = isolate->GetCurrentContext(); + + Local data = info.Data().As(); + SKObstack obstack = data->Get(context, 0) + .ToLocalChecked() + .As() + ->Value(); + Local resolver = + data->Get(context, 1) + .ToLocalChecked() + .As(); + + SKIP_destroy_Obstack(obstack); + + Local error = info[0]; + resolver->Reject(context, error).ToChecked(); + }, + handlerData) + .ToLocalChecked(); + + promise->Then(context, onFulfilled, onRejected).ToLocalChecked(); + + args.GetReturnValue().Set(wrapperPromise); + } else { + SKIP_destroy_Obstack(obstack); + args.GetReturnValue().Set(result); + } +} + void GetErrorObject(const v8::FunctionCallbackInfo& args) { Isolate* isolate = args.GetIsolate(); HandleScope scope(isolate); diff --git a/skipruntime-ts/addon/src/common.h b/skipruntime-ts/addon/src/common.h index 44c2257eb..24d21e4c4 100644 --- a/skipruntime-ts/addon/src/common.h +++ b/skipruntime-ts/addon/src/common.h @@ -35,6 +35,7 @@ char* CallJSNullableStringFunction(v8::Isolate*, v8::Local, const char*, int, v8::Local[]); void NatTryCatch(v8::Isolate*, std::function); void RunWithGC(const v8::FunctionCallbackInfo&); +void UnsafeAsyncRunWithGC(const v8::FunctionCallbackInfo&); void GetErrorObject(const v8::FunctionCallbackInfo&); char* ToSKString(v8::Isolate*, v8::Local); void Print(v8::Isolate*, const char*, v8::Local); @@ -70,5 +71,6 @@ struct SkipException : std::exception { #define SKNotifier void* #define SKReducer void* #define SKMapper void* +#define SKMergeState void* #endif // SKCOMMON_H diff --git a/skipruntime-ts/addon/src/index.ts b/skipruntime-ts/addon/src/index.ts index 96597b88f..52a31898b 100644 --- a/skipruntime-ts/addon/src/index.ts +++ b/skipruntime-ts/addon/src/index.ts @@ -14,6 +14,7 @@ type AddOn = { getSkipRuntimeFromBinding: () => SkipRuntimeFromBinding; initSkipRuntimeToBinding: (binding: ToBinding) => void; runWithGC: (fn: () => T) => T; + unsafeAsyncRunWithGC: (fn: () => Promise) => Promise; getErrorObject: (skExc: Pointer) => Error; }; @@ -27,6 +28,7 @@ const fromBinding = skip_runtime.getSkipRuntimeFromBinding(); const tobinding = new ToBinding( fromBinding, skip_runtime.runWithGC, + skip_runtime.unsafeAsyncRunWithGC, () => jsonConverter, skip_runtime.getErrorObject, ); diff --git a/skipruntime-ts/addon/src/main.cc b/skipruntime-ts/addon/src/main.cc index 3e29c815c..036a48337 100644 --- a/skipruntime-ts/addon/src/main.cc +++ b/skipruntime-ts/addon/src/main.cc @@ -26,6 +26,8 @@ void InitToBinding(const v8::FunctionCallbackInfo& args) { void Initialize(v8::Local exports, v8::Local module, void* context) { NODE_SET_METHOD(exports, "runWithGC", skbinding::RunWithGC); + NODE_SET_METHOD(exports, "unsafeAsyncRunWithGC", + skbinding::UnsafeAsyncRunWithGC); NODE_SET_METHOD(exports, "getErrorObject", skbinding::GetErrorObject); NODE_SET_METHOD(exports, "getJsonBinding", skjson::GetBinding); NODE_SET_METHOD(exports, "getSkipRuntimeFromBinding", diff --git a/skipruntime-ts/addon/src/tojs.cc b/skipruntime-ts/addon/src/tojs.cc index d55fc9ed5..bafe2a4d6 100644 --- a/skipruntime-ts/addon/src/tojs.cc +++ b/skipruntime-ts/addon/src/tojs.cc @@ -56,6 +56,10 @@ CJSON SkipRuntime_Runtime__getForKey(char* resource, CJObject jsonParams, CJSON SkipRuntime_Runtime__update(char* input, CJSON values); double SkipRuntime_Runtime__fork(char* input); double SkipRuntime_Runtime__merge(CJArray); +SKMergeState SkipRuntime_Runtime__startMerge(CJArray); +double SkipRuntime_Runtime__endMerge(SKMergeState); +double SkipRuntime_Runtime__abortMerge(SKMergeState); + double SkipRuntime_Runtime__abortFork(); uint32_t SkipRuntime_Runtime__forkExists(char* input); CJSON SkipRuntime_Runtime__reload(SKService service); @@ -902,6 +906,72 @@ void MergeOfRuntime(const FunctionCallbackInfo& args) { }); } +void StartMergeOfRuntime(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + HandleScope scope(isolate); + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException( + Exception::TypeError(FromUtf8(isolate, "Must have one parameter."))); + return; + }; + if (!args[0]->IsExternal()) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + FromUtf8(isolate, "The parameter must be a pointer."))); + return; + }; + NatTryCatch(isolate, [&args](Isolate* isolate) { + SKMergeState skstate = + SkipRuntime_Runtime__startMerge(args[0].As()->Value()); + args.GetReturnValue().Set(External::New(isolate, skstate)); + }); +} + +void EndMergeOfRuntime(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + HandleScope scope(isolate); + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException( + Exception::TypeError(FromUtf8(isolate, "Must have one parameter."))); + return; + }; + if (!args[0]->IsExternal()) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + FromUtf8(isolate, "The parameter must be a pointer."))); + return; + }; + NatTryCatch(isolate, [&args](Isolate* isolate) { + double skerror = + SkipRuntime_Runtime__endMerge(args[0].As()->Value()); + args.GetReturnValue().Set(Number::New(isolate, skerror)); + }); +} + +void AbortMergeOfRuntime(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + HandleScope scope(isolate); + if (args.Length() != 1) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException( + Exception::TypeError(FromUtf8(isolate, "Must have one parameter."))); + return; + }; + if (!args[0]->IsExternal()) { + // Throw an Error that is passed back to JavaScript + isolate->ThrowException(Exception::TypeError( + FromUtf8(isolate, "The parameter must be a pointer."))); + return; + }; + NatTryCatch(isolate, [&args](Isolate* isolate) { + double skerror = + SkipRuntime_Runtime__abortMerge(args[0].As()->Value()); + args.GetReturnValue().Set(Number::New(isolate, skerror)); + }); +} + void AbortForkOfRuntime(const FunctionCallbackInfo& args) { Isolate* isolate = args.GetIsolate(); HandleScope scope(isolate); @@ -1043,6 +1113,12 @@ void GetToJSBinding(const FunctionCallbackInfo& args) { AddFunction(isolate, binding, "SkipRuntime_Runtime__update", UpdateOfRuntime); AddFunction(isolate, binding, "SkipRuntime_Runtime__fork", ForkOfRuntime); AddFunction(isolate, binding, "SkipRuntime_Runtime__merge", MergeOfRuntime); + AddFunction(isolate, binding, "SkipRuntime_Runtime__startMerge", + StartMergeOfRuntime); + AddFunction(isolate, binding, "SkipRuntime_Runtime__endMerge", + EndMergeOfRuntime); + AddFunction(isolate, binding, "SkipRuntime_Runtime__abortMerge", + AbortMergeOfRuntime); AddFunction(isolate, binding, "SkipRuntime_Runtime__abortFork", AbortForkOfRuntime); AddFunction(isolate, binding, "SkipRuntime_Runtime__forkExists", diff --git a/skipruntime-ts/core/src/api.ts b/skipruntime-ts/core/src/api.ts index 2a8505bc8..b546e74ad 100644 --- a/skipruntime-ts/core/src/api.ts +++ b/skipruntime-ts/core/src/api.ts @@ -483,6 +483,11 @@ export interface Resource< ): EagerCollection; } +export interface Store { + load(): Promise[]>; + save(data: Entry[]): Promise; +} + /** * Initial data for a service's input collections. * @@ -492,8 +497,8 @@ export interface Resource< */ export type InitialData = { [Name in keyof Inputs]: Inputs[Name] extends EagerCollection - ? Entry[] - : Entry[]; + ? Entry[] | Store + : Entry[] | Store; }; /** diff --git a/skipruntime-ts/core/src/binding.ts b/skipruntime-ts/core/src/binding.ts index 5cf107793..6e31ac39c 100644 --- a/skipruntime-ts/core/src/binding.ts +++ b/skipruntime-ts/core/src/binding.ts @@ -175,6 +175,15 @@ export interface FromBinding { SkipRuntime_Runtime__merge( ignore: Pointer>, ): Handle; + SkipRuntime_Runtime__startMerge( + ignore: Pointer>, + ): Pointer; + SkipRuntime_Runtime__endMerge( + state: Pointer, + ): Handle; + SkipRuntime_Runtime__abortMerge( + state: Pointer, + ): Handle; SkipRuntime_Runtime__abortFork(): Handle; SkipRuntime_Runtime__forkExists(name: string): boolean; diff --git a/skipruntime-ts/core/src/index.ts b/skipruntime-ts/core/src/index.ts index fe48d59cd..c93ec3abc 100644 --- a/skipruntime-ts/core/src/index.ts +++ b/skipruntime-ts/core/src/index.ts @@ -37,6 +37,7 @@ import { type SkipService, type Watermark, type ExternalService, + type Store, } from "./api.js"; import { @@ -122,7 +123,14 @@ export class ServiceDefinition { if (!this.service.initialData) throw new Error(`No initial data defined.`); const data = this.service.initialData[name]; if (!data) throw new Error(`Initial data '${name}' not exist.`); - return data; + return Array.isArray(data) ? data : Array.of(); + } + + getStore(name: string): Nullable> { + if (!this.service.initialData) throw new Error(`No initial data defined.`); + const data = this.service.initialData[name]; + if (!data) throw new Error(`Initial data '${name}' not exist.`); + return !Array.isArray(data) ? data : null; } createGraph( @@ -754,8 +762,25 @@ export class ServiceInstance { const uuid = crypto.randomUUID(); const fork = this.fork(uuid); try { + const normalized = entries; await fork.update_(collection, entries); - fork.merge([]); + if (!this.forkName) { + const store = this.definition.getStore(collection); + if (store) { + await this.refs.unsafeAsyncRunWithGC(async () => { + const state = this.refs.binding.SkipRuntime_Runtime__startMerge( + this.refs.json().exportJSON([]), + ); + try { + await store.save(normalized); + this.refs.binding.SkipRuntime_Runtime__endMerge(state); + } catch (ex: unknown) { + this.refs.binding.SkipRuntime_Runtime__abortMerge(state); + throw ex; + } + }); + } else fork.merge([]); + } else fork.merge([]); } catch (ex: unknown) { fork.abortFork(); throw ex; @@ -858,7 +883,6 @@ export class ServiceInstance { * @returns The forked ServiceInstance */ private fork(name: string): ServiceInstance { - if (this.forkName) throw new Error(`Unable to fork ${this.forkName}.`); this.refs.setFork(this.forkName); this.refs.fork(name); return new ServiceInstance(this.refs, name, this.definition); @@ -970,6 +994,7 @@ export class ToBinding { constructor( public binding: FromBinding, public runWithGC: (fn: () => T) => T, + public unsafeAsyncRunWithGC: (fn: () => Promise) => Promise, private getConverter: () => JsonConverter, private getError: (skExc: Pointer) => Error, ) { @@ -1362,6 +1387,14 @@ export class ToBinding { const skservice = this.binding.SkipRuntime_createService(skservicehHdl); return this.binding.SkipRuntime_initService(skservice); }); + const instance = new ServiceInstance(this, uuid, definition); + if (service.initialData) { + for (const [name, store] of Object.entries(service.initialData)) { + if (!Array.isArray(store)) { + await instance.update(name, await store.load()); + } + } + } this.setFork(uuid); this.merge(); return new ServiceInstance(this, null, definition); diff --git a/skipruntime-ts/core/src/internal.ts b/skipruntime-ts/core/src/internal.ts index 3b3f7dbd3..04f4bb7d9 100644 --- a/skipruntime-ts/core/src/internal.ts +++ b/skipruntime-ts/core/src/internal.ts @@ -50,3 +50,6 @@ export type Reducer = T; declare const notifier: unique symbol; export type Notifier = T; + +declare const mergestate: unique symbol; +export type MergeState = T; diff --git a/skipruntime-ts/skiplang/ffi/src/FFI.sk b/skipruntime-ts/skiplang/ffi/src/FFI.sk index 5767903c2..c95563322 100644 --- a/skipruntime-ts/skiplang/ffi/src/FFI.sk +++ b/skipruntime-ts/skiplang/ffi/src/FFI.sk @@ -727,6 +727,39 @@ fun mergeForkForOfRuntime(streamsToIgnore: SKJSON.CJArray): Float { } } +@export("SkipRuntime_Runtime__startMerge") +fun startMergeForOfRuntime( + streamsToIgnore: SKJSON.CJArray, +): ?SKStore.MergeState { + streamsToIgnore match { + | SKJSON.CJArray(values) -> + ignore = values.iterator().map(SKJSON.asString).collect(Set); + getFork().map(f -> + SKStore.startMergeForkNoGc(f, SKStore.ForkSettings::keepall(ignore)) + ) + } +} + +@export("SkipRuntime_Runtime__endMerge") +fun endMergeForOfRuntime(state: ?SKStore.MergeState): Float { + try { + state.each(s -> SKStore.endMergeForkNoGc(s)); + 0.0 + } catch { + | err -> getErrorHdl(err) + } +} + +@export("SkipRuntime_Runtime__abortMerge") +fun abortMergeForOfRuntime(state: ?SKStore.MergeState): Float { + try { + state.each(s -> SKStore.abortMergeForkNoGc(s)); + 0.0 + } catch { + | err -> getErrorHdl(err) + } +} + @export("SkipRuntime_Runtime__abortFork") fun abortForkOfRuntime(): Float { try { diff --git a/skipruntime-ts/tests/src/tests.ts b/skipruntime-ts/tests/src/tests.ts index 32224a46d..4c81d10b6 100644 --- a/skipruntime-ts/tests/src/tests.ts +++ b/skipruntime-ts/tests/src/tests.ts @@ -19,6 +19,7 @@ import type { Nullable, Reducer, ChangeManager, + Store, } from "@skipruntime/core"; import { LoadStatus } from "@skipruntime/core"; import { Count, Sum } from "@skipruntime/helpers"; @@ -1221,6 +1222,40 @@ class WithChanges implements ChangeManager { } } +export class MockStore implements Store { + constructor(private data: Map) {} + + load(): Promise[]> { + return Promise.resolve(Array.from(this.data.entries())); + } + + save(data: Entry[]): Promise { + if (data.filter((entry) => entry[0] === "error").length > 0) { + return Promise.reject(new Error("Something goes wrong.")); + } + data.forEach((item) => this.data.set(item[0], item[1])); + return Promise.resolve(); + } +} + +function mapWithStoreService(): SkipService { + return { + initialData: { + input: new MockStore( + new Map([ + ["v1", [3]], + ["v2", [5]], + ]), + ), + }, + resources: { map1: Map1Resource }, + + createGraph(inputCollections: Input_SN) { + return inputCollections; + }, + }; +} + export function initTests( category: string, initService: (service: SkipService) => Promise, @@ -2227,4 +2262,36 @@ INSERT INTO skip_test (id, x) VALUES (1, 1), (2, 2), (3, 3);`); ); } }); + + it("testMapWithStore", async () => { + const resource = "map1"; + const skipservice = mapWithStoreService(); + const service = await initService(skipservice); + expect(await service.getAll(resource)).toEqual([ + ["v1", [5]], + ["v2", [7]], + ]); + await service.update("input", [["v3", [10]]]); + expect( + await (skipservice.initialData!.input as Store).load(), + ).toEqual([ + ["v1", [3]], + ["v2", [5]], + ["v3", [10]], + ]); + expect(await service.getArray("map1", "v3")).toEqual([12]); + try { + await service.update("input", [ + ["error", [-5]], + ["v3", [50]], + ]); + throw new Error("Error was not thrown"); + } catch (e: unknown) { + expect(e).toBeA(Error); + expect((e as Error).message).toMatchRegex( + new RegExp(/^(?:Error: )?Something goes wrong.$/), + ); + } + await service.close(); + }); } diff --git a/skipruntime-ts/wasm/src/internals/skipruntime_module.ts b/skipruntime-ts/wasm/src/internals/skipruntime_module.ts index c0711eb0b..b346677a3 100644 --- a/skipruntime-ts/wasm/src/internals/skipruntime_module.ts +++ b/skipruntime-ts/wasm/src/internals/skipruntime_module.ts @@ -190,6 +190,13 @@ export interface FromWasm { SkipRuntime_Runtime__merge( ignore: ptr>, ): Handle; + SkipRuntime_Runtime__startMerge( + ignore: ptr>, + ): ptr; + SkipRuntime_Runtime__endMerge(state: ptr): Handle; + SkipRuntime_Runtime__abortMerge( + state: ptr, + ): Handle; SkipRuntime_Runtime__abortFork(): Handle; SkipRuntime_Runtime__forkExists(name: ptr): number; SkipRuntime_Runtime__reload( @@ -684,6 +691,24 @@ export class WasmFromBinding implements FromBinding { return this.fromWasm.SkipRuntime_Runtime__merge(toPtr(ignore)); } + SkipRuntime_Runtime__startMerge( + ignore: ptr>, + ): ptr { + return this.fromWasm.SkipRuntime_Runtime__startMerge(toPtr(ignore)); + } + + SkipRuntime_Runtime__endMerge( + state: ptr, + ): Handle { + return this.fromWasm.SkipRuntime_Runtime__endMerge(toPtr(state)); + } + + SkipRuntime_Runtime__abortMerge( + state: ptr, + ): Handle { + return this.fromWasm.SkipRuntime_Runtime__abortMerge(toPtr(state)); + } + SkipRuntime_Runtime__abortFork(): Handle { return this.fromWasm.SkipRuntime_Runtime__abortFork(); } @@ -763,6 +788,7 @@ class LinksImpl implements Links { this.tobinding = new ToBinding( fromBinding, utils.runWithGc.bind(utils), + utils.unsafeAsyncWithGc.bind(utils), () => (this.env.shared.get("SKJSON")! as SKJSONShared).converter, (skExc: Pointer) => this.utils.getErrorObject(toPtr(skExc)),