@ringozz/godot 4.7.1-12 → 4.7.1-13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ringozz/godot",
3
3
  "author": "Vladimir Davidovich",
4
- "version": "4.7.1-12",
4
+ "version": "4.7.1-13",
5
5
  "description": "Node-API bindings for Godot Engine — call Godot classes from JavaScript",
6
6
  "keywords": [
7
7
  "godot"
@@ -38,7 +38,7 @@
38
38
  "optionalDependencies": {
39
39
  "@ringozz/godot-macos-arm64": "^4.7.1-3",
40
40
  "@ringozz/godot-windows-x86_64": "^4.7.1-3",
41
- "@ringozz/godot-web-wasm32": "^4.7.1-11"
41
+ "@ringozz/godot-web-wasm32": "^4.7.1-12"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@types/bun": "*"
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@ import { GodotInstance } from '../gen/classes/GodotInstance.ts';
10
10
  import { SceneTree } from '../gen/classes/SceneTree.ts';
11
11
  import { Window } from '../gen/classes/Window.ts';
12
12
  import { gc } from './debug.ts';
13
- import { cancelAnimationFrame, getGodot, requestAnimationFrame } from './runtime.ts';
13
+ import { cancelAnimationFrame, cleanupHooks, getGodot, requestAnimationFrame } from './runtime.ts';
14
14
 
15
15
  // ---- make sure these classes are not tree-shaked ----
16
16
  void ValueTypes;
@@ -34,7 +34,19 @@ export async function runGodot(signal?: AbortSignal, unmount?: () => PromiseLike
34
34
  await new Promise(requestAnimationFrame);
35
35
 
36
36
  await unmount?.();
37
- console.log(gc());
37
+ while (cleanupHooks.size) {
38
+ const hooks = Array.from(cleanupHooks); cleanupHooks.clear();
39
+ await Promise.all(hooks.map((hook) => hook()));
40
+ }
41
+
42
+ // Let pending Napi wrapper finalizers run before tearing down the engine:
43
+ // Bun.gc(true) marks unreachable wrappers but their ~GodotVar/~Variant
44
+ // (which unrefs RefCounted objects) only runs on later macrotasks — and one
45
+ // wrapper's finalizer can free others. Freeing the engine first would report
46
+ // every still-referenced RefCounted as leaked, so drain a few gc+tick rounds.
47
+ for (let i = 0; i < 4; i++) {
48
+ console.log(gc());
49
+ await new Promise((resolve) => setTimeout(resolve, 0));
50
+ }
38
51
  return godot.free();
39
52
  }
40
-
package/src/load.ts CHANGED
@@ -8,9 +8,25 @@ import { ProjectSettings } from '../gen/classes/ProjectSettings.ts';
8
8
  import { ResourceLoader, ThreadLoadStatus } from '../gen/classes/ResourceLoader.ts';
9
9
  import { ResourceUID } from '../gen/classes/ResourceUID.ts';
10
10
  import type { Resource } from '../gen/classes/Resource.ts';
11
- import { stageFile } from './runtime.ts';
11
+ import { cleanupHooks, stageFile } from './runtime.ts';
12
12
  import { decodeCtex } from './web-image.ts';
13
13
 
14
+ // Each loaded wrapper is released at shutdown via a `cleanupHooks` entry
15
+ // holding a WeakRef: module-scope asset imports (the preload modules' memoized
16
+ // load promises) keep their wrappers reachable, so GC alone can't collect them
17
+ // before the engine stops. WeakRef keeps the entries from pinning resources
18
+ // during the app's lifetime.
19
+ function track<T extends Resource>(res: T): T {
20
+ const ref = new WeakRef<Resource>(res);
21
+ cleanupHooks.add(() => {
22
+ const r = ref.deref();
23
+ if (r) {
24
+ try { r.free(); } catch {}
25
+ }
26
+ });
27
+ return res;
28
+ }
29
+
14
30
  /**
15
31
  * A Godot `Resource` subclass constructor: its `.name` is the registered class
16
32
  * name (used as the `ResourceLoader` type hint), and `InstanceType<C>` is the
@@ -153,6 +169,7 @@ export function loadAsset<C extends ResourceConstructor>(
153
169
  data: Record<string, unknown> = {},
154
170
  ): { materialize: Promise<unknown>; load: Promise<InstanceType<C>> } {
155
171
  const cached = cachedResource<C>(path);
172
+ if (cached) cached.then(track);
156
173
  const materialize = cached ? Promise.resolve() : materializeFiles(files, deps);
157
174
  const existing = data[path] as Promise<InstanceType<C>> | undefined;
158
175
  const load = existing ?? cached ?? materialize.then(() => loadResourceAsync(path, cls, files));
@@ -189,6 +206,10 @@ export async function loadResourceAsync<C extends ResourceConstructor>(
189
206
  break;
190
207
  }
191
208
  if (status === ThreadLoadStatus.THREAD_LOAD_FAILED || status === ThreadLoadStatus.THREAD_LOAD_INVALID_RESOURCE) {
209
+ // Collect the engine's LoadToken so a failed load doesn't leave it
210
+ // registered (a bare `RefCounted` leaked at exit). Safe no-op when
211
+ // no token exists.
212
+ ResourceLoader.loadThreadedGet(path);
192
213
  throw new Error(`loadResourceAsync(${path}): load failed (status ${status})`);
193
214
  }
194
215
  await nextTick();
@@ -206,5 +227,5 @@ export async function loadResourceAsync<C extends ResourceConstructor>(
206
227
  }
207
228
  }
208
229
  }
209
- return result;
230
+ return track(result);
210
231
  }
package/src/runtime.ts CHANGED
@@ -79,3 +79,6 @@ export function _P(signal: any, onfulfilled?: any, onrejected?: any) {
79
79
  if (res !== 0) reject(new Error('Failed to connect signal: error code ' + res));
80
80
  }).then(onfulfilled, onrejected);
81
81
  }
82
+
83
+ /** Cleanup callbacks run once by `runGodot` just before the engine is freed; may be async. */
84
+ export const cleanupHooks: Set<() => void | Promise<void>> = new Set();