@volter/twin-fal 0.1.1 → 0.1.2
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/dist/src/fal-server.js +5 -3
- package/dist/src/fal-twin.js +4 -1
- package/dist/src/fal-webhooks.js +4 -3
- package/package.json +3 -3
- package/src/fal-server.ts +5 -3
- package/src/fal-twin.ts +3 -1
- package/src/fal-webhooks.ts +4 -3
package/dist/src/fal-server.js
CHANGED
|
@@ -23,13 +23,15 @@
|
|
|
23
23
|
// construction — loudly, never a silent fallback.
|
|
24
24
|
import { serveHttp } from '@volter/world-core';
|
|
25
25
|
import { handleFalTwinRequest } from "./fal-twin.js";
|
|
26
|
-
import { createTwinFetchFromHandler, twinManifest } from '@volter/world-core';
|
|
26
|
+
import { createTwinFetchFromHandler, twinManifest, withRequestScopes } from '@volter/world-core';
|
|
27
27
|
import { createFalScenarioEngine, loadFalScenarioDocument } from "./fal-scenario.js";
|
|
28
28
|
export function createFalTwinFetch(options = {}) {
|
|
29
29
|
const scenarioPath = options.scenarioPath ?? process.env.TWIN_FAL_SCENARIO;
|
|
30
30
|
const scenarioEngine = scenarioPath ? createFalScenarioEngine(loadFalScenarioDocument(scenarioPath)) : undefined;
|
|
31
31
|
const { scenarioPath: _scenarioPath, ...handlerOptions } = options;
|
|
32
|
-
|
|
32
|
+
// a read-only request (x-volter-read-only) is a request to a read-only twin: a status poll answers the
|
|
33
|
+
// request's current status without advancing it, nothing is written (withRequestScopes: the pack opts in)
|
|
34
|
+
return withRequestScopes(createTwinFetchFromHandler(handleFalTwinRequest, {
|
|
33
35
|
...handlerOptions,
|
|
34
36
|
manifest: () => twinManifest({
|
|
35
37
|
vendor: 'fal',
|
|
@@ -41,7 +43,7 @@ export function createFalTwinFetch(options = {}) {
|
|
|
41
43
|
}),
|
|
42
44
|
scenarioStatus: () => (scenarioEngine ? scenarioEngine.status() : { vendor: 'fal', handlers: [], misses: 0, recentMisses: [] }),
|
|
43
45
|
...(scenarioEngine ? { handlerOptions: { scenarioEngine } } : {}),
|
|
44
|
-
});
|
|
46
|
+
}), { refuse: () => Response.json({ detail: 'twin is read-only; omit readOnly to accept writes' }, { status: 405 }) });
|
|
45
47
|
}
|
|
46
48
|
export async function createFalTwinServer(options = {}) {
|
|
47
49
|
const server = await serveHttp({
|
package/dist/src/fal-twin.js
CHANGED
|
@@ -301,7 +301,10 @@ async function handleStatus(modelId, requestId, includeLogs, req) {
|
|
|
301
301
|
const existing = getRow('request', requestId, req.root);
|
|
302
302
|
if (!existing)
|
|
303
303
|
return detailNotFound('Request not found.');
|
|
304
|
-
|
|
304
|
+
// ADVANCE ON POLL — one step per status check. A read-only twin (a World's read token) answers the
|
|
305
|
+
// request's current status and never advances it: the app's next poll sees what it would have.
|
|
306
|
+
if (!req.readOnly)
|
|
307
|
+
await progressOnce(requestId, req);
|
|
305
308
|
const row = getRow('request', requestId, req.root);
|
|
306
309
|
return { status: 200, body: statusView(row, modelId, requestId, includeLogs) };
|
|
307
310
|
}
|
package/dist/src/fal-webhooks.js
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
//
|
|
41
41
|
// `node:crypto` is required LAZILY (server-only), same convention as replicate-webhooks.ts, so
|
|
42
42
|
// this module stays browser-bundle safe.
|
|
43
|
-
import { applyTwinWriteAtomic, projectResources, nodeBuiltin, worldNow } from '@volter/world-core';
|
|
43
|
+
import { applyTwinWriteAtomic, projectResources, nodeBuiltin, worldNow, runAsVendorMove } from '@volter/world-core';
|
|
44
44
|
function nodeCrypto() {
|
|
45
45
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
46
46
|
return nodeBuiltin('node:crypto');
|
|
@@ -79,7 +79,8 @@ async function ensureFalSigningSeed(root) {
|
|
|
79
79
|
const fast = storedSeed(projectResources(SERVICE, root));
|
|
80
80
|
if (fast)
|
|
81
81
|
return fast;
|
|
82
|
-
|
|
82
|
+
// fal's own signing key: a vendor move, made on first use whoever asks (a read-only request too)
|
|
83
|
+
const { value } = await runAsVendorMove(() => applyTwinWriteAtomic(SERVICE, (resources) => {
|
|
83
84
|
const existing = storedSeed(resources);
|
|
84
85
|
if (existing)
|
|
85
86
|
return { kind: 'skip', value: existing };
|
|
@@ -95,7 +96,7 @@ async function ensureFalSigningSeed(root) {
|
|
|
95
96
|
actor: { kind: 'system' },
|
|
96
97
|
},
|
|
97
98
|
};
|
|
98
|
-
}, root);
|
|
99
|
+
}, root));
|
|
99
100
|
return value;
|
|
100
101
|
}
|
|
101
102
|
/** The per-root ed25519 keypair, derived from the PERSISTED seed. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/twin-fal",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Local fal.ai twin for the model-inference queue lifecycle (submit/status/response/cancel on queue.fal.run, the sync fal.run variant, host-split routing, ed25519-signed webhooks) built on @volter/world-core.",
|
|
5
5
|
"author": "Volter (https://github.com/volter-ai)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@volter/world-core": "2.0.
|
|
38
|
+
"@volter/world-core": "2.0.2"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/bun": "^1.2.20",
|
|
42
42
|
"@types/node": "^24.0.0",
|
|
43
|
-
"@volter/world-core": "2.0.
|
|
43
|
+
"@volter/world-core": "2.0.2",
|
|
44
44
|
"@volter/world-tooling": "0.1.0",
|
|
45
45
|
"@fal-ai/client": "^1.10.1",
|
|
46
46
|
"typescript": "^5.9.0"
|
package/src/fal-server.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// construction — loudly, never a silent fallback.
|
|
24
24
|
import { serveHttp } from '@volter/world-core';
|
|
25
25
|
import { handleFalTwinRequest } from './fal-twin.ts';
|
|
26
|
-
import { createTwinFetchFromHandler, twinManifest } from '@volter/world-core';
|
|
26
|
+
import { createTwinFetchFromHandler, twinManifest, withRequestScopes } from '@volter/world-core';
|
|
27
27
|
import { createFalScenarioEngine, loadFalScenarioDocument, type FalScenarioEngine } from './fal-scenario.ts';
|
|
28
28
|
|
|
29
29
|
/** Options every fal-twin HTTP surface needs, independent of who owns the socket. */
|
|
@@ -39,7 +39,9 @@ export function createFalTwinFetch(options: FalTwinFetchOptions = {}): (request:
|
|
|
39
39
|
const scenarioPath = options.scenarioPath ?? process.env.TWIN_FAL_SCENARIO;
|
|
40
40
|
const scenarioEngine: FalScenarioEngine | undefined = scenarioPath ? createFalScenarioEngine(loadFalScenarioDocument(scenarioPath)) : undefined;
|
|
41
41
|
const { scenarioPath: _scenarioPath, ...handlerOptions } = options;
|
|
42
|
-
|
|
42
|
+
// a read-only request (x-volter-read-only) is a request to a read-only twin: a status poll answers the
|
|
43
|
+
// request's current status without advancing it, nothing is written (withRequestScopes: the pack opts in)
|
|
44
|
+
return withRequestScopes(createTwinFetchFromHandler(handleFalTwinRequest, {
|
|
43
45
|
...handlerOptions,
|
|
44
46
|
manifest: () => twinManifest({
|
|
45
47
|
vendor: 'fal',
|
|
@@ -51,7 +53,7 @@ export function createFalTwinFetch(options: FalTwinFetchOptions = {}): (request:
|
|
|
51
53
|
}),
|
|
52
54
|
scenarioStatus: () => (scenarioEngine ? scenarioEngine.status() : { vendor: 'fal', handlers: [], misses: 0, recentMisses: [] }),
|
|
53
55
|
...(scenarioEngine ? { handlerOptions: { scenarioEngine } } : {}),
|
|
54
|
-
});
|
|
56
|
+
}), { refuse: () => Response.json({ detail: 'twin is read-only; omit readOnly to accept writes' }, { status: 405 }) });
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
export async function createFalTwinServer(options: { root?: string; port?: number; readOnly?: boolean; scenarioPath?: string } = {}): Promise<{ port: number; stop: () => void }> {
|
package/src/fal-twin.ts
CHANGED
|
@@ -352,7 +352,9 @@ async function handleSubmit(modelId: string, rawBody: string | undefined, surfac
|
|
|
352
352
|
async function handleStatus(modelId: string, requestId: string, includeLogs: boolean, req: FalRequest): Promise<FalResponse> {
|
|
353
353
|
const existing = getRow('request', requestId, req.root);
|
|
354
354
|
if (!existing) return detailNotFound('Request not found.');
|
|
355
|
-
|
|
355
|
+
// ADVANCE ON POLL — one step per status check. A read-only twin (a World's read token) answers the
|
|
356
|
+
// request's current status and never advances it: the app's next poll sees what it would have.
|
|
357
|
+
if (!req.readOnly) await progressOnce(requestId, req);
|
|
356
358
|
const row = getRow('request', requestId, req.root)!;
|
|
357
359
|
return { status: 200, body: statusView(row, modelId, requestId, includeLogs) };
|
|
358
360
|
}
|
package/src/fal-webhooks.ts
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
//
|
|
41
41
|
// `node:crypto` is required LAZILY (server-only), same convention as replicate-webhooks.ts, so
|
|
42
42
|
// this module stays browser-bundle safe.
|
|
43
|
-
import { applyTwinWriteAtomic, projectResources, nodeBuiltin, worldNow } from '@volter/world-core';
|
|
43
|
+
import { applyTwinWriteAtomic, projectResources, nodeBuiltin, worldNow, runAsVendorMove } from '@volter/world-core';
|
|
44
44
|
|
|
45
45
|
type NodeCrypto = typeof import('node:crypto');
|
|
46
46
|
function nodeCrypto(): NodeCrypto {
|
|
@@ -96,7 +96,8 @@ function storedSeed(resources: readonly { type: string; id: string }[]): string
|
|
|
96
96
|
async function ensureFalSigningSeed(root?: string): Promise<string> {
|
|
97
97
|
const fast = storedSeed(projectResources(SERVICE, root));
|
|
98
98
|
if (fast) return fast;
|
|
99
|
-
|
|
99
|
+
// fal's own signing key: a vendor move, made on first use whoever asks (a read-only request too)
|
|
100
|
+
const { value } = await runAsVendorMove(() => applyTwinWriteAtomic<string>(SERVICE, (resources) => {
|
|
100
101
|
const existing = storedSeed(resources);
|
|
101
102
|
if (existing) return { kind: 'skip', value: existing };
|
|
102
103
|
const seed = nodeCrypto().randomBytes(SEED_BYTES).toString('base64');
|
|
@@ -111,7 +112,7 @@ async function ensureFalSigningSeed(root?: string): Promise<string> {
|
|
|
111
112
|
actor: { kind: 'system' },
|
|
112
113
|
},
|
|
113
114
|
};
|
|
114
|
-
}, root);
|
|
115
|
+
}, root));
|
|
115
116
|
return value;
|
|
116
117
|
}
|
|
117
118
|
|