# quickjs-worker Migration Plan

## Goal

Align `quickjs-worker` with `../deno-director` where the API is runtime-agnostic, while explicitly dropping Deno-specific, V8-specific, and Node-interop-specific behavior.

The short answer: this is feasible, but only if the migration is treated as:

1. a bridge contract upgrade
2. a wrapper/API port
3. a selective feature drop

It is not a straight copy/paste of the whole repo.

## Bottom Line

You are not crazy.

`quickjs-worker` already has the same foundational shape:

- native runtime bridge
- JS wrapper object
- `eval` / `evalSync`
- module loading
- injected globals and host callbacks
- `postMessage`
- memory/limit controls

That is enough to preserve a majority of `deno-director`'s ergonomic API surface.

What is not portable is the subset that depends on:

- Deno permissions
- Deno runtime APIs
- V8 heap/inspector internals
- Node compatibility / CommonJS interop as implemented in Deno
- stream transport built around the current native protocol

## High-Level Judgment

### Portable with modest changes

- `eval`, `evalSync`, `lastExecutionStats`
- richer `module.*` host API
- `global.*` API
- `handle.*` API
- startup/bootstrap sequencing
- lifecycle/runtime eventing
- wrapper-level stats and error enrichment
- better option normalization and naming
- module namespace proxying instead of `moduleReturn(...)`

### Portable, but only after bridge changes

- rich wire serialization
- nested object/function injection
- module namespace proxy wrappers
- stable handle identities and handle disposal
- better error fidelity
- object graph identity / cyclic references

### Should be dropped or redesigned

- Deno permissions model
- `env` and `cwd` APIs as Deno runtime features
- TS/JSX source loaders unless you add a host transpile layer
- Node compatibility flags
- remote import policy matching Deno semantics
- stream transport from `deno-director`
- V8 heap space stats and inspector support
- `DenoDirector` as-is

## Repo Comparison

## quickjs-worker today

Current public surface is small and workable:

- `eval`
- `evalSync`
- `evalModule`
- `setGlobal`
- `postMessage`
- `getByteCode`
- `loadByteCode`
- `gc`
- `memory`
- `close`

Relevant files:

- [`index.js`](/home/slott/Developer/quickjs-worker/index.js)
- [`index.d.ts`](/home/slott/Developer/quickjs-worker/index.d.ts)
- [`src/lib.rs`](/home/slott/Developer/quickjs-worker/src/lib.rs)
- [`src/qjs.rs`](/home/slott/Developer/quickjs-worker/src/qjs.rs)
- [`src/js_data.rs`](/home/slott/Developer/quickjs-worker/src/js_data.rs)

Current bridge strengths:

- async and sync eval
- promise resolution for top-level returned promises
- module loading callback
- host callback globals
- console forwarding
- memory and interrupt controls

Current bridge weaknesses:

- narrow data model
- no wire protocol abstraction
- no handle abstraction
- no module namespace proxying
- `evalModule` depends on `moduleReturn(...)`
- object/function injection is ad hoc

## deno-director today

Relevant files:

- [`src/ts/types.ts`](/home/slott/Developer/deno-director/src/ts/types.ts)
- [`src/ts/worker.ts`](/home/slott/Developer/deno-director/src/ts/worker.ts)
- [`src/ts/wire.ts`](/home/slott/Developer/deno-director/src/ts/wire.ts)

The important thing about `deno-director` is that a lot of the value is in the TypeScript wrapper, not the Deno runtime itself.

The wrapper provides:

- stable, layered APIs
- handle-based access to runtime object graphs
- module namespace proxies
- richer events and stats
- normalized options
- host-side ergonomics and policy hooks

That part is mostly transferable in design.

## Gap Matrix

| Area | quickjs-worker today | deno-director equivalent | Migration judgment |
|---|---|---|---|
| Core eval | Present | Present | Preserve |
| Async promise resolution | Present | Present | Preserve |
| Module eval | Present, but `moduleReturn(...)` based | Present with namespace proxy | Redesign |
| Module import API | Minimal callback loader | Rich `module.import/eval/register/clear` | Port with native adjustments |
| Global set/get | `setGlobal` only | full `global.*` API | Port |
| Handles | Missing | full `handle.*` API | Port after bridge work |
| Messaging | Basic `postMessage` + event | richer wrapper/events | Port subset |
| Console routing | Present | Present | Preserve |
| Memory stats | Present, QuickJS-specific | Present, V8-specific shape | Preserve with QuickJS shape |
| Limits | Present | Present | Preserve |
| Wire serialization | narrow enum | rich graph-aware wire format | Redesign |
| Deno permissions | Missing | Present | Drop |
| TS/JSX loaders | Missing | Present | Optional host-side add-on |
| Node compat | Missing | Present | Drop |
| Streams | Missing | Present | Drop initially |
| Director fleet manager | Missing | Present | Maybe later, wrapper-only |

## The Biggest Technical Constraint

The main blocker is not module loading. It is the value bridge.

`quickjs-worker` currently serializes values through `JsDataTypes`, which covers:

- primitives
- JSON objects
- arrays
- `Date`
- `Uint8Array`
- `Error`

That is enough for the current API, but it is not enough for the `deno-director` wrapper model.

`deno-director` assumes a host-side wire format that can encode:

- `undefined`
- `NaN`, `Infinity`, `-0`
- `bigint`
- `RegExp`
- `Map`
- `Set`
- `ArrayBuffer` and typed arrays
- shared/cyclic object graphs
- error metadata including `cause`

Without that, you can still copy API names, but behavior will drift badly.

## Recommendation: Upgrade the Bridge First

Before porting a lot of wrapper code, introduce a proper wire protocol for `quickjs-worker`.

### Target bridge principles

- Native code should stop treating most objects as plain JSON blobs.
- JS wrapper should own dehydration/hydration rules.
- Native bridge should carry either:
  - already-serialized wire objects, or
  - a richer native enum matching the wire model.
- Errors should be transported as structured values, not stringified exceptions.

### Best path

Adopt the `deno-director` wire model as the conceptual template, not as a literal copy.

Reason:

- The TypeScript `dehydrateForWire` / `hydrateFromWire` logic in [`src/ts/wire.ts`](/home/slott/Developer/deno-director/src/ts/wire.ts) is portable in design.
- But the current QuickJS Rust layer expects native enums and JSON parsing, so some adaptation is required.

## Recommendation: Ditch `moduleReturn(...)`

This should be changed.

The `moduleReturn(...)` convention is a dead-end if the goal is API compatibility with `deno-director`.

### Why it should go

- It is a custom calling convention users must remember.
- It prevents normal ESM mental models.
- It blocks `module.import(...)` and `module.eval(...)` from returning namespace-like objects naturally.
- It is incompatible with reusing `deno-director`’s module wrapper design.

### Target behavior

`module.eval(source)` should return a proxy object representing the module namespace.

Examples:

```ts
const mod = await worker.module.eval(`
  export const answer = 42;
  export function add(a, b) { return a + b; }
`);

console.log(mod.answer); // 42
console.log(await mod.add(1, 2)); // 3
```

```ts
await worker.module.register("app:math", `
  export const mul = (a, b) => a * b;
`);

const math = await worker.module.import("app:math");
console.log(await math.mul(3, 4)); // 12
```

### How to implement it

The right model is the same one `deno-director` uses:

1. evaluate or import a module in the runtime
2. obtain the resulting namespace object
3. create a host-side proxy around that namespace
4. route property reads and function calls back into the runtime

For QuickJS, that means:

- native module evaluation/import must return a stable runtime reference to the namespace object
- the JS wrapper must expose that through handle-like operations
- `wrapModuleNamespace(...)` logic can be adapted after a handle layer exists

### Important consequence

You probably should not implement module namespace proxies before implementing handles.

Module proxies are basically a specialized handle wrapper.

## API Surface Recommendation

## Keep and evolve

### Worker class

Keep a single runtime class, but rename only if you want stronger parity.

Reasonable options:

- keep `QuickJS`
- add alias `QuickJSWorker`
- optionally export default class similar to `DenoWorker`

### Eval API

Keep:

- `eval`
- `evalSync`

Change:

- normalize options to a stable `EvalOptions`
- align `type` naming to `"script" | "module"` instead of `"classic" | "module"`
- allow `srcLoader` later only if host transpilation is added

### Module API

Replace:

- `evalModule(code)` as the primary API

With:

- `worker.module.eval(source, options?)`
- `worker.module.import(specifier)`
- `worker.module.register(name, source, options?)`
- `worker.module.clear(name)`

Compatibility path:

- keep `evalModule(...)` temporarily as sugar over `worker.module.eval(...)`
- deprecate `moduleReturn(...)`

### Global API

Expand from:

- `setGlobal(key, value)`

To:

- `global.set`
- `global.get`
- `global.has`
- `global.delete`
- `global.call`
- `global.construct`
- `global.keys`
- `global.entries`
- `global.toJSON`
- `global.getType`

This is highly portable because the hard part is just runtime-side property traversal plus a handle bridge.

### Handle API

Add:

- `handle.get(path)`
- `handle.tryGet(path)`
- `handle.eval(source)`
- per-handle methods like `get`, `set`, `call`, `await`, `dispose`, `toJSON`

This is one of the highest-value ports from `deno-director`.

It gives:

- module namespace proxies
- stable references to runtime values
- fewer serialization round-trips
- access to complex runtime object graphs without flattening everything into JSON

### Events

Today you have:

- `message`
- `close`

Add a small subset of `deno-director` events:

- `runtime`
- `error`

Do not overbuild this initially. The event model is wrapper code and easy to add later.

## Features To Drop Explicitly

These should be marked out of scope for parity:

- Deno permission model
- Deno `env` / `cwd` runtime APIs
- Deno inspector support
- Deno remote import semantics
- Deno-specific source loaders
- Node compatibility controls from `deno-director`
- stream transport

This matters because otherwise the migration target stays fuzzy and the project scope explodes.

## Features To Consider Replacing with QuickJS Equivalents

### Memory stats

Keep `memory()`, but do not mimic V8 heap shape.

Use a QuickJS-specific shape and document it clearly.

### Bytecode

`quickjs-worker` already has bytecode support. That is a QuickJS-native advantage and should remain part of the API even though `deno-director` does not center on it.

### Limits

Keep and standardize:

- max eval time
- max memory
- max stack
- interrupt count

Potential future normalization:

- group these under `limits`

## Concrete Migration Plan

## Phase 1: Define the new public API

Create a target API document before implementation.

Suggested public shape:

```ts
class QuickJSWorker {
  eval(source, options?)
  evalSync(source, options?)
  postMessage(value)
  close(options?)
  isClosed()
  memory()
  gc()

  module: {
    eval(source, options?)
    import(specifier)
    register(name, source, options?)
    clear(name)
  }

  global: {
    set(path, value)
    get(path)
    has(path)
    delete(path)
    call(path, args?)
    construct(path, args?)
    keys(path?)
    entries(path?)
    toJSON(path?)
    getType(path?)
  }

  handle: {
    get(path, options?)
    tryGet(path, options?)
    eval(source, options?)
  }
}
```

## Phase 2: Replace `JsDataTypes` as the main abstraction

The current enum is too narrow to support the target API cleanly.

Options:

### Option A: JS-owned wire protocol

Native layer transports plain JS values that are already dehydrated by the wrapper.

Pros:

- closer to `deno-director`
- easier wrapper reuse

Cons:

- requires careful host/runtime marshaling for callbacks and errors

### Option B: richer Rust enum

Expand `JsDataTypes` to match the wire model.

Pros:

- keeps more logic native

Cons:

- more Rust work
- duplicates the JS wire model

Recommended: hybrid leaning toward JS-owned wire semantics, because it makes wrapper parity easier.

## Phase 3: Add runtime-side handle bridge

Port the design of the handle runtime bridge from `deno-director`, but adapt naming and assumptions for QuickJS.

Needed capabilities:

- resolve global path
- get/set/delete nested properties
- call functions
- construct values
- await promise-like values
- inspect type metadata
- retain stable references by handle id
- dispose handle ids

This can be implemented mostly as injected JS inside the QuickJS realm.

That is good news: much of this is runtime-agnostic glue.

## Phase 4: Rebuild module API on top of handles

After handles exist:

- `module.import(specifier)` imports and stores namespace handle
- `module.eval(source)` evaluates module and returns wrapped namespace
- `module.register` stores source in a host-side registry visible to import loader
- `module.clear` removes stored source

At this point, `moduleReturn(...)` can be removed.

## Phase 5: Port selected wrapper ergonomics

Port the smaller, runtime-agnostic parts of `deno-director`:

- better error enrichment
- startup sequencing
- richer events
- option normalization
- wrapper stats

## Copy/Paste Reuse Assessment

## Probably reusable with moderate edits

- handle bridge JS source logic
- module wrapper concepts
- `global.*` / `handle.*` TS surface
- some error/context handling
- event naming and wrapper structure
- option normalization patterns

## Reusable in design, but not literal code

- wire serialization
- module registration/import flow
- module namespace proxying

These need adaptation to the QuickJS native contract.

## Not worth copying

- Deno permission and env/cwd systems
- Node compatibility code
- stream transport
- V8 heap/stat interfaces
- inspector integration

## Major Risks

### Risk 1: pretending JSON is enough

If the bridge stays JSON-first, the copied wrapper API will look richer than it really is.

That will produce subtle bugs in:

- functions
- shared object identity
- nested mutation
- module namespace behavior
- non-JSON built-ins

### Risk 2: implementing module proxies before handles

You can fake this briefly, but it will become a dead-end.

Module proxies should be built on the same underlying handle/reference mechanism as everything else.

### Risk 3: preserving old and new APIs too long

If `evalModule` + `moduleReturn(...)` remains the primary path while `module.*` is added beside it, the code will fork conceptually.

Better:

- keep old API as compatibility shim
- mark it as deprecated
- move all new implementation to `module.*`

## Recommended Near-Term Decisions

1. Commit to dropping `moduleReturn(...)`.
2. Commit to a handle-based architecture.
3. Preserve the broad API shape of `deno-director` only for runtime-agnostic areas.
4. Do not attempt parity for Deno permissions, Node interop, streams, or inspector.
5. Treat the wire/value bridge as the first-class migration task.

## Suggested Final Scope

If the project is successful, the result should feel like:

"`deno-director`'s ergonomic host API, but backed by QuickJS and stripped of Deno/V8/Node-interop-specific features."

Not:

"a clone of `deno-director` that happens to run QuickJS."

That distinction is what keeps this realistic.

## Recommended Next Implementation Step

Implement a minimal vertical slice:

1. add `handle.eval(...)`
2. add runtime-side handle ids
3. add `module.eval(...)` returning a namespace proxy
4. keep `evalModule(...)` as a shim
5. remove the need for `moduleReturn(...)` in new code

If that slice works, the broader migration is very likely viable.
