@hoardodile/host 0.1.1 → 0.1.3

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.
@@ -337,7 +337,14 @@ export function createPluginSandbox(
337
337
  await teardownChild(previous)
338
338
  }
339
339
  if (!opts.eager) {
340
- void teardownChild(state)
340
+ // AWAITED, not fire-and-forget: the hook list was probed by
341
+ // this load, and the loader returns the sandboxed definition
342
+ // the moment we resolve. A caller that invokes a hook
343
+ // immediately after (e.g. the post-install `onInstall`) must
344
+ // not find a half-alive child: `ensureLoaded` would see the
345
+ // not-yet-torn-down child and post its invocation into a
346
+ // dying process (rejected as "worker stopped").
347
+ await teardownChild(state)
341
348
  }
342
349
  return createSandboxedPlugin(state.hooks ?? ["detect"], (hook, api) =>
343
350
  invoke(state, hook, api),
@@ -611,6 +618,28 @@ export function createPluginSandbox(
611
618
  throw new Error(`plugin ${state.id} sandbox unavailable`)
612
619
  }
613
620
 
621
+ try {
622
+ return await invokeOnChild(state, child, hook, api)
623
+ } finally {
624
+ // An install hook is one-shot and the host may immediately
625
+ // re-commit the same plugin (an update): a kept-alive worker
626
+ // holding the plugin directory (Windows keeps directory ops
627
+ // pinned while a child is alive) would make the vault-move
628
+ // during the next commit fail with EPERM. Drop the child right
629
+ // after an `onInstall`; the next invocation respawns it.
630
+ if (hook === "onInstall") {
631
+ state.respawnTimes = []
632
+ await teardownChild(state)
633
+ }
634
+ }
635
+ }
636
+
637
+ function invokeOnChild(
638
+ state: PluginState,
639
+ child: ChildProcess,
640
+ hook: HookName,
641
+ api: ResourceAPI,
642
+ ): Promise<unknown> {
614
643
  const callId = nextCallId++
615
644
  return new Promise((resolveCall, reject) => {
616
645
  const call: PendingCall = {
@@ -43,6 +43,7 @@ const HOOK_NAMES = [
43
43
  "coverLocal",
44
44
  "listFiles",
45
45
  "imageHashes",
46
+ "onInstall",
46
47
  ]
47
48
 
48
49
  const API_METHOD_NAMES = [
@@ -283,7 +284,11 @@ function send(message) {
283
284
  function approxByteSize(value) {
284
285
  if (value instanceof Uint8Array) return value.byteLength
285
286
  try {
286
- return JSON.stringify(value).length * 2
287
+ // `undefined` (hooks that legitimately return nothing — e.g.
288
+ // `onInstall`) stringifies to `undefined`, not a string: an empty
289
+ // payload must size 0, never throw into the Infinity branch.
290
+ const text = JSON.stringify(value)
291
+ return (typeof text === "string" ? text : "").length * 2
287
292
  } catch {
288
293
  return Infinity
289
294
  }