@ringozz/godot 4.7.1-11 → 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/README.md CHANGED
@@ -74,7 +74,7 @@ const tex = await loadResourceAsync('res://textures/icon.jpg', Texture2D); //
74
74
  - `loadResourceAsync(path, cls, files?)` — generic over the Godot class: `cls.name` is the `ResourceLoader` type hint, and the return type is `InstanceType<typeof cls>`. Loads in the background (`loadThreadedRequest` + status polling). Godot default arguments apply: the napi dispatch truncates trailing `undefined` args, so `loadThreadedRequest` uses its default `CACHE_MODE_REUSE` and the resource stays cached in the engine (scene re-references don't re-read the file). On web it only works for paths already staged into MEMFS (generated asset modules handle this). When the asset's `files` map is passed, the staged product files (`.ctex`/`.scn`/native sources) are deleted from MEMFS after the resource loads — only the `.import` sidecars are kept, since they route imported source paths to their products. The decoded texture bytes don't linger in the JS heap.
75
75
  - `loadAsset(path, cls, files, deps, data)` — the generated asset modules' entry point (also usable directly): returns `{ materialize, load }`. It checks the engine's `ResourceCache` first (when the asset is already loaded, `load` resolves to the cached instance and staging is skipped), stages the bundled files via Godot's `copyToFS` (zero-copy handover of the fetched bytes; a no-op on desktop where the files already exist), loads via `loadResourceAsync`, and memoizes `load` on `data` (pass `import.meta.hot.data`) so HMR re-evaluations reuse the same fulfilled promise.
76
76
 
77
- Asset imports are resolved by `@ringozz/godot/preload` (a Bun plugin, registered via `bunfig.toml` preload for `bun run`/`bun test` and via `[serve.static] plugins` for `Bun.serve`'s fullstack HTML routes; the module's default export is the single plugin object) into a module per asset:
77
+ Asset imports are resolved by `@ringozz/godot/preload` (a Bun plugin, registered via `bunfig.toml` preload for `bun run`/`bun test` and via `[serve.static] plugins` for `Bun.serve`'s fullstack HTML routes; the module's default export is the single plugin object) into a module per asset. Only **JS imports** are virtualized — HTML asset references (`<link rel="icon">`, `<img src>`) and CSS `url()` refs fall through to Bun's normal (content-hashed) asset handling, so favicons and other web assets co-exist with Godot imports:
78
78
 
79
79
  ```ts
80
80
  import foo from './models/foo.gltf';
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-11",
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-10"
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/preload.ts CHANGED
@@ -175,6 +175,22 @@ const assetPlugin: BunPlugin = {
175
175
  name: 'godot-assets',
176
176
  setup(build) {
177
177
  build.onResolve({ filter: ASSET_RE }, (args) => {
178
+ // Only JS module imports of Godot source assets become `godot`
179
+ // modules. `Bun.build` reports JS imports as `import-statement`
180
+ // (and `dynamic-import`/`require-*`) but HTML asset references
181
+ // (<link>/<img>) and CSS url() refs as `internal`/`url-token` —
182
+ // those fall through so Bun copies/hashes the file normally. The
183
+ // runtime preload (bun run / bun test) reports JS imports with no
184
+ // kind (`undefined`), so treat that as a JS import too.
185
+ if (
186
+ args.kind !== undefined &&
187
+ args.kind !== 'import-statement' &&
188
+ args.kind !== 'dynamic-import' &&
189
+ args.kind !== 'require-call' &&
190
+ args.kind !== 'require-resolve'
191
+ ) {
192
+ return;
193
+ }
178
194
  const importerAbs = args.importer?.startsWith(`${NS}:`)
179
195
  ? args.importer.slice(NS.length + 1)
180
196
  : args.importer;
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();