Skip to content

Commit 1b8b4e4

Browse files
committed
feat(handler)!: rename to initDevframe returning a DevframeInstance
The factory is named for the instance it initiates (define → init pairing with defineDevframe), and the web-standard request handler is reached as a property — initDevframe(def).handler — matching the content.handler mounting model, so future capabilities extend the instance object instead of overloading a handler-named factory. - createHandler → initDevframe; CreateHandlerOptions → InitDevframeOptions - DevframeHandler → DevframeInstance; fetch → handler - diagnostics/docs updated (DF0053/DF0054 wording)
1 parent 0bf31cc commit 1b8b4e4

7 files changed

Lines changed: 123 additions & 123 deletions

File tree

docs/errors/DF0053.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,32 @@
22
outline: deep
33
---
44

5-
# DF0053: Memoized Handler Replaced
5+
# DF0053: Memoized Instance Replaced
66

77
## Message
88

9-
> createHandler("`{id}`") replaced the live handler memoized under key "`{key}`": its options changed since the previous call.
9+
> initDevframe("`{id}`") replaced the live instance memoized under key "`{key}`": its options changed since the previous call.
1010
1111
## Cause
1212

13-
`createHandler` was called with a `key` that already maps to a live handler, but the option fingerprint differs from the memoized instance's. Dev servers that re-evaluate modules on the fly (Next.js, Nitro, SvelteKit HMR) re-run `createHandler` on every reload; the `key` memoization normally returns the live instance, but when the options genuinely changed the old instance — including its side-car WebSocket server — is closed and a fresh one starts.
13+
`initDevframe` was called with a `key` that already maps to a live instance, but the option fingerprint differs from the memoized one's. Dev servers that re-evaluate modules on the fly (Next.js, Nitro, SvelteKit HMR) re-run `initDevframe` on every reload; the `key` memoization normally returns the live instance, but when the options genuinely changed the old instance — including its side-car WebSocket server — is closed and a fresh one starts.
1414

1515
## Example
1616

1717
```ts
18-
import { createHandler } from 'devframe/handler'
18+
import { initDevframe } from 'devframe/handler'
1919

2020
// First evaluation:
21-
createHandler(def, { key: 'devtools', ws: { port: 7811 } })
21+
initDevframe(def, { key: 'devtools', ws: { port: 7811 } })
2222

2323
// A later reload with a different port replaces the live instance:
24-
createHandler(def, { key: 'devtools', ws: { port: 7812 } }) // ⚠ DF0053
24+
initDevframe(def, { key: 'devtools', ws: { port: 7812 } }) // ⚠ DF0053
2525
```
2626

2727
## Fix
2828

29-
This is informational when you edited the options on purpose — the replacement is the intended behavior. If it fires without an intentional change, make the options stable across reloads (module-level constants rather than values recomputed per evaluation), or give genuinely different handlers distinct keys.
29+
This is informational when you edited the options on purpose — the replacement is the intended behavior. If it fires without an intentional change, make the options stable across reloads (module-level constants rather than values recomputed per evaluation), or give genuinely different instances distinct keys.
3030

3131
## Source
3232

33-
- [`packages/devframe/src/adapters/handler.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/adapters/handler.ts)`createHandler` warns this before closing and replacing a memoized instance whose options fingerprint changed.
33+
- [`packages/devframe/src/adapters/handler.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/adapters/handler.ts)`initDevframe` warns this before closing and replacing a memoized instance whose options fingerprint changed.

docs/errors/DF0054.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,32 @@
22
outline: deep
33
---
44

5-
# DF0054: connectionMeta() Before Handler Ready
5+
# DF0054: connectionMeta() Before Instance Ready
66

77
## Message
88

9-
> connectionMeta() was called before createHandler("`{id}`") finished initializing.
9+
> connectionMeta() was called before initDevframe("`{id}`") finished initializing.
1010
1111
## Cause
1212

13-
`createHandler` is a synchronous factory that kicks off asynchronous initialization eagerly — running `def.setup`, binding the WebSocket tier, and mounting the routes. `connectionMeta()` describes the WebSocket binding, which only exists once that initialization completes; calling it earlier has nothing correct to return.
13+
`initDevframe` is a synchronous factory that kicks off asynchronous initialization eagerly — running `def.setup`, binding the WebSocket tier, and mounting the routes. `connectionMeta()` describes the WebSocket binding, which only exists once that initialization completes; calling it earlier has nothing correct to return.
1414

1515
## Example
1616

1717
```ts
18-
import { createHandler } from 'devframe/handler'
18+
import { initDevframe } from 'devframe/handler'
1919

20-
const handler = createHandler(def)
21-
handler.connectionMeta() // ✗ throws DF0054 — init is still in flight
20+
const devtools = initDevframe(def)
21+
devtools.connectionMeta() // ✗ throws DF0054 — init is still in flight
2222

23-
await handler.ready
24-
handler.connectionMeta() // ✓ { backend: 'websocket', websocket: { … } }
23+
await devtools.ready
24+
devtools.connectionMeta() // ✓ { backend: 'websocket', websocket: { … } }
2525
```
2626

2727
## Fix
2828

29-
Await `handler.ready` (or any `handler.fetch` call — it awaits readiness internally) before reading `connectionMeta()`.
29+
Await `instance.ready` (or any request through `instance.handler` — it awaits readiness internally) before reading `connectionMeta()`.
3030

3131
## Source
3232

33-
- [`packages/devframe/src/adapters/handler.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/adapters/handler.ts)`createHandler`'s `connectionMeta()` throws this while initialization is still pending.
33+
- [`packages/devframe/src/adapters/handler.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/adapters/handler.ts)`initDevframe`'s `connectionMeta()` throws this while initialization is still pending.

packages/devframe/src/adapters/__tests__/handler.test.ts

Lines changed: 45 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { describe, expect, it, vi } from 'vitest'
1010
import { WebSocket } from 'ws'
1111
import { getTempAuthCode } from '../../node/auth/state'
1212
import { defineDevframe } from '../../types/devframe'
13-
import { createHandler } from '../handler'
13+
import { initDevframe } from '../handler'
1414

1515
const HANDSHAKE = { authToken: '', ua: 'test', origin: 'http://localhost' }
1616

@@ -43,43 +43,43 @@ function defineTestDef(id: string) {
4343

4444
describe('adapters/handler', () => {
4545
it('connectionMeta() before ready throws DF0054', () => {
46-
const handler = createHandler(defineTestDef('handler-early'), { auth: false })
47-
expect(() => handler.connectionMeta()).toThrow(/DF0054|finished initializing/)
48-
return handler.close()
46+
const devtools = initDevframe(defineTestDef('handler-early'), { auth: false })
47+
expect(() => devtools.connectionMeta()).toThrow(/DF0054|finished initializing/)
48+
return devtools.close()
4949
})
5050

5151
it('default tier: eager side-car — SPA, meta, and WS RPC through fetch', async () => {
5252
const distDir = makeTmpDist()
5353
const wsPort = await getPort({ port: 18110, host: '127.0.0.1' })
54-
const handler = createHandler(defineTestDef('handler-test'), {
54+
const devtools = initDevframe(defineTestDef('handler-test'), {
5555
auth: false,
5656
distDir,
5757
host: '127.0.0.1',
5858
ws: { port: wsPort },
5959
})
6060

6161
try {
62-
await handler.ready
62+
await devtools.ready
6363
// The advertised meta carries the side-car port with the unified route.
64-
expect(handler.connectionMeta()).toEqual({
64+
expect(devtools.connectionMeta()).toEqual({
6565
backend: 'websocket',
6666
websocket: { port: wsPort, path: '__ws' },
6767
})
6868

6969
// Hosted default base: /__<id>/.
70-
const index = await handler.fetch(new Request('http://localhost:3000/__handler-test/'))
70+
const index = await devtools.handler(new Request('http://localhost:3000/__handler-test/'))
7171
expect(index.status).toBe(200)
7272
expect(await index.text()).toContain('handler test')
7373

74-
const metaRes = await handler.fetch(new Request('http://localhost:3000/__handler-test/__connection.json'))
74+
const metaRes = await devtools.handler(new Request('http://localhost:3000/__handler-test/__connection.json'))
7575
expect(metaRes.status).toBe(200)
7676
expect(await metaRes.json()).toEqual({
7777
backend: 'websocket',
7878
websocket: { port: wsPort, path: '__ws' },
7979
})
8080

8181
// Outside the base — and inside it on a miss — the fetch surface 404s.
82-
const outside = await handler.fetch(new Request('http://localhost:3000/app'))
82+
const outside = await devtools.handler(new Request('http://localhost:3000/app'))
8383
expect(outside.status).toBe(404)
8484

8585
// RPC round-trips against the side-car.
@@ -88,7 +88,7 @@ describe('adapters/handler', () => {
8888
client.$close()
8989
}
9090
finally {
91-
await handler.close()
91+
await devtools.close()
9292
}
9393

9494
// Teardown is real: the side-car no longer accepts connections.
@@ -102,21 +102,21 @@ describe('adapters/handler', () => {
102102
it('gates by default: untrusted calls reject until the OTP exchange', async () => {
103103
const wsPort = await getPort({ port: 18120, host: '127.0.0.1' })
104104
const spy = vi.spyOn(console, 'log').mockImplementation(() => {})
105-
const handler = createHandler(defineTestDef('handler-auth'), {
105+
const devtools = initDevframe(defineTestDef('handler-auth'), {
106106
host: '127.0.0.1',
107107
ws: { port: wsPort },
108108
})
109109

110110
try {
111-
await handler.ready
111+
await devtools.ready
112112
// The banner waits for the public origin: unknown until a request
113113
// arrives, then printed exactly once (the magic link points at the
114114
// origin the handler is actually mounted on).
115115
expect(spy).not.toHaveBeenCalled()
116-
await handler.fetch(new Request('http://localhost:4321/__handler-auth/__connection.json'))
116+
await devtools.handler(new Request('http://localhost:4321/__handler-auth/__connection.json'))
117117
expect(spy).toHaveBeenCalledTimes(1)
118118
expect(String(spy.mock.calls[0])).toContain('http://localhost:4321')
119-
await handler.fetch(new Request('http://localhost:4321/__handler-auth/__connection.json'))
119+
await devtools.handler(new Request('http://localhost:4321/__handler-auth/__connection.json'))
120120
expect(spy).toHaveBeenCalledTimes(1)
121121

122122
const client = connectWsClient(`ws://127.0.0.1:${wsPort}/__ws`)
@@ -132,7 +132,7 @@ describe('adapters/handler', () => {
132132
}
133133
finally {
134134
spy.mockRestore()
135-
await handler.close()
135+
await devtools.close()
136136
}
137137
})
138138

@@ -141,27 +141,27 @@ describe('adapters/handler', () => {
141141
const host = '127.0.0.1'
142142
const port = await getPort({ port: 18130, host })
143143

144-
let handlerRef!: ReturnType<typeof createHandler>
144+
let devtoolsRef!: ReturnType<typeof initDevframe>
145145
const server = createServer((req, res) => {
146146
// The middleware self-filters by base; everything else stays the
147147
// host app's.
148-
handlerRef.nodeMiddleware(req, res, () => {
148+
devtoolsRef.nodeMiddleware(req, res, () => {
149149
res.statusCode = 418
150150
res.end('host app')
151151
})
152152
})
153-
handlerRef = createHandler(defineTestDef('handler-shared'), {
153+
devtoolsRef = initDevframe(defineTestDef('handler-shared'), {
154154
auth: false,
155155
distDir,
156156
server,
157157
})
158158
await new Promise<void>(resolve => server.listen(port, host, resolve))
159159

160160
try {
161-
await handlerRef.ready
161+
await devtoolsRef.ready
162162
// Zero extra ports: the meta advertises a same-origin relative route,
163163
// resolved against __connection.json's own URL.
164-
expect(handlerRef.connectionMeta()).toEqual({
164+
expect(devtoolsRef.connectionMeta()).toEqual({
165165
backend: 'websocket',
166166
websocket: { path: '__ws' },
167167
})
@@ -201,7 +201,7 @@ describe('adapters/handler', () => {
201201
expect(offOpened).toBe(false)
202202
}
203203
finally {
204-
await handlerRef.close()
204+
await devtoolsRef.close()
205205
// Fire-and-forget teardown for the host-owned test server: the
206206
// deliberately dangling off-route upgrade socket sits outside the
207207
// http server's tracked connections, so a graceful close never
@@ -212,87 +212,87 @@ describe('adapters/handler', () => {
212212
})
213213

214214
it('ws.url tier: advertises the external endpoint verbatim, owns no transport', async () => {
215-
const handler = createHandler(defineTestDef('handler-remote'), {
215+
const devtools = initDevframe(defineTestDef('handler-remote'), {
216216
ws: { url: 'wss://devtools.example.com/relay/__ws' },
217217
})
218218

219219
try {
220-
await handler.ready
221-
expect(handler.connectionMeta()).toEqual({
220+
await devtools.ready
221+
expect(devtools.connectionMeta()).toEqual({
222222
backend: 'websocket',
223223
websocket: 'wss://devtools.example.com/relay/__ws',
224224
})
225225
}
226226
finally {
227-
await handler.close()
227+
await devtools.close()
228228
}
229229
})
230230

231231
it('tunnel pattern: ws.url with a server binds locally, advertises the relay', async () => {
232232
const host = '127.0.0.1'
233233
const port = await getPort({ port: 18170, host })
234-
let handlerRef!: ReturnType<typeof createHandler>
234+
let devtoolsRef!: ReturnType<typeof initDevframe>
235235
const server = createServer((req, res) => {
236-
handlerRef.nodeMiddleware(req, res)
236+
devtoolsRef.nodeMiddleware(req, res)
237237
})
238-
handlerRef = createHandler(defineTestDef('handler-tunnel'), {
238+
devtoolsRef = initDevframe(defineTestDef('handler-tunnel'), {
239239
auth: false,
240240
server,
241241
ws: { url: 'wss://devtools.example.com/relay/__ws' },
242242
})
243243
await new Promise<void>(resolve => server.listen(port, host, resolve))
244244

245245
try {
246-
await handlerRef.ready
246+
await devtoolsRef.ready
247247
// The browser is told to dial the relay…
248-
expect(handlerRef.connectionMeta().websocket).toBe('wss://devtools.example.com/relay/__ws')
248+
expect(devtoolsRef.connectionMeta().websocket).toBe('wss://devtools.example.com/relay/__ws')
249249
// …while the local socket keeps serving (the relay's forward target).
250250
const client = connectWsClient(`ws://${host}:${port}/__handler-tunnel/__ws`)
251251
await expect(client.$call('test:probe' as any)).resolves.toBe('ok')
252252
client.$close()
253253
}
254254
finally {
255-
await handlerRef.close()
255+
await devtoolsRef.close()
256256
server.close()
257257
server.closeAllConnections()
258258
}
259259
})
260260

261261
it('mcp: mounts <base>__mcp and advertises it in the meta', async () => {
262262
const wsPort = await getPort({ port: 18140, host: '127.0.0.1' })
263-
const handler = createHandler(defineTestDef('handler-mcp'), {
263+
const devtools = initDevframe(defineTestDef('handler-mcp'), {
264264
auth: false,
265265
mcp: true,
266266
ws: { port: wsPort },
267267
})
268268

269269
try {
270-
await handler.ready
271-
expect(handler.connectionMeta().mcp).toEqual({ path: '__mcp' })
270+
await devtools.ready
271+
expect(devtools.connectionMeta().mcp).toEqual({ path: '__mcp' })
272272
// The route is mounted: a bare GET is answered by the MCP transport
273273
// (405 for a session-less GET), not the 404 an unmounted path gets.
274-
const res = await handler.fetch(new Request('http://localhost:3000/__handler-mcp/__mcp', {
274+
const res = await devtools.handler(new Request('http://localhost:3000/__handler-mcp/__mcp', {
275275
headers: { origin: 'http://localhost:3000' },
276276
}))
277277
expect(res.status).not.toBe(404)
278278
}
279279
finally {
280-
await handler.close()
280+
await devtools.close()
281281
}
282282
})
283283

284284
it('key memoization: re-runs return the live instance; changed options replace it', async () => {
285285
const def = defineTestDef('handler-memo')
286286
const wsPort = await getPort({ port: 18150, host: '127.0.0.1' })
287-
const a = createHandler(def, { auth: false, key: 'memo-test', host: '127.0.0.1', ws: { port: wsPort } })
288-
const b = createHandler(def, { auth: false, key: 'memo-test', host: '127.0.0.1', ws: { port: wsPort } })
287+
const a = initDevframe(def, { auth: false, key: 'memo-test', host: '127.0.0.1', ws: { port: wsPort } })
288+
const b = initDevframe(def, { auth: false, key: 'memo-test', host: '127.0.0.1', ws: { port: wsPort } })
289289
expect(b).toBe(a)
290290

291291
try {
292292
await a.ready
293293
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
294294
const wsPort2 = await getPort({ port: 18151, host: '127.0.0.1' })
295-
const c = createHandler(def, { auth: false, key: 'memo-test', host: '127.0.0.1', ws: { port: wsPort2 } })
295+
const c = initDevframe(def, { auth: false, key: 'memo-test', host: '127.0.0.1', ws: { port: wsPort2 } })
296296
try {
297297
expect(c).not.toBe(a)
298298
expect(String(warn.mock.calls)).toContain('DF0053')
@@ -323,22 +323,22 @@ describe('adapters/handler', () => {
323323

324324
it('bridge mode: without a distDir only meta + WS are served', async () => {
325325
const wsPort = await getPort({ port: 18160, host: '127.0.0.1' })
326-
const handler = createHandler(defineTestDef('handler-bridge'), {
326+
const devtools = initDevframe(defineTestDef('handler-bridge'), {
327327
auth: false,
328328
ws: { port: wsPort },
329329
})
330330

331331
try {
332-
await handler.ready
333-
const meta = await handler.fetch(new Request('http://localhost:3000/__handler-bridge/__connection.json'))
332+
await devtools.ready
333+
const meta = await devtools.handler(new Request('http://localhost:3000/__handler-bridge/__connection.json'))
334334
expect(meta.status).toBe(200)
335335
// No SPA mount: the base itself is a miss, normalized to a bare 404.
336-
const spa = await handler.fetch(new Request('http://localhost:3000/__handler-bridge/'))
336+
const spa = await devtools.handler(new Request('http://localhost:3000/__handler-bridge/'))
337337
expect(spa.status).toBe(404)
338338
expect(await spa.text()).toBe('')
339339
}
340340
finally {
341-
await handler.close()
341+
await devtools.close()
342342
}
343343
})
344344
})

0 commit comments

Comments
 (0)