@flue/sdk 0.3.2 → 0.3.4
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/index.mjs +107 -17
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -127,6 +127,83 @@ function validateUserWranglerConfig(config) {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
/**
|
|
130
|
+
* Compute Flue's migration contributions for a build.
|
|
131
|
+
*
|
|
132
|
+
* Algorithm:
|
|
133
|
+
*
|
|
134
|
+
* 1. Walk every existing migration entry in the user's config and union the
|
|
135
|
+
* SQLite-backed classes it declares — across `new_sqlite_classes` and
|
|
136
|
+
* the `to` side of `renamed_classes` / `transferred_classes`. The
|
|
137
|
+
* resulting set is "already-declared": every SQLite-backed class
|
|
138
|
+
* Cloudflare's runtime currently knows about for this Worker.
|
|
139
|
+
* `deleted_classes` and the `from` side of renames are subtracted, since
|
|
140
|
+
* they've been explicitly removed.
|
|
141
|
+
*
|
|
142
|
+
* KV-backed classes (`new_classes`) are deliberately NOT added to the
|
|
143
|
+
* "declared" set. Flue agents always need a SQLite-backed class for
|
|
144
|
+
* session storage; if a user happens to have a KV-backed DO with the
|
|
145
|
+
* same name as a Flue agent, we still need to emit our SQLite migration.
|
|
146
|
+
* The deploy itself will then surface a clear "class already defined"
|
|
147
|
+
* error from Cloudflare rather than silently shipping a broken worker
|
|
148
|
+
* where the agent has no working session store.
|
|
149
|
+
* 2. For each class in `currentClasses` that isn't already-declared, emit a
|
|
150
|
+
* deterministic per-class migration: one tag, one class. Per-class tags
|
|
151
|
+
* are essential because Cloudflare migration tags are immutable once
|
|
152
|
+
* deployed — packing all classes under a single shared tag (the original
|
|
153
|
+
* bug in issue #15) means new classes added on a redeploy are silently
|
|
154
|
+
* ignored. With per-class tags, every redeploy is a no-op except for
|
|
155
|
+
* the truly net-new classes.
|
|
156
|
+
*
|
|
157
|
+
* Renames and deletes are not auto-detected. If an agent file disappears,
|
|
158
|
+
* Flue silently emits no migration for it — Cloudflare's runtime keeps the
|
|
159
|
+
* orphaned class data alive but unbound, and the user can clean up (or
|
|
160
|
+
* rename to recover) by adding a manual `renamed_classes` / `deleted_classes`
|
|
161
|
+
* migration to their own wrangler.jsonc. Auto-emitting `deleted_classes`
|
|
162
|
+
* would destroy stored DO data on every accidental file removal, which is
|
|
163
|
+
* never the right default.
|
|
164
|
+
*
|
|
165
|
+
* Returned in alphabetical order so a regenerated `dist/wrangler.jsonc` is
|
|
166
|
+
* byte-identical across machines and CI runs.
|
|
167
|
+
*
|
|
168
|
+
* Pure function: takes the current class list + the user's existing
|
|
169
|
+
* migrations array (typically `userConfig.migrations` straight from
|
|
170
|
+
* wrangler's reader) and returns the migrations to append. Doesn't read or
|
|
171
|
+
* write any files.
|
|
172
|
+
*/
|
|
173
|
+
function computeFlueMigrations(currentClasses, userMigrations) {
|
|
174
|
+
const migrationsArray = Array.isArray(userMigrations) ? userMigrations : [];
|
|
175
|
+
const declared = /* @__PURE__ */ new Set();
|
|
176
|
+
for (const raw of migrationsArray) {
|
|
177
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
178
|
+
const m = raw;
|
|
179
|
+
const collectClassList = (key) => {
|
|
180
|
+
const v = m[key];
|
|
181
|
+
return Array.isArray(v) ? v.filter((c) => typeof c === "string") : [];
|
|
182
|
+
};
|
|
183
|
+
for (const c of collectClassList("new_sqlite_classes")) declared.add(c);
|
|
184
|
+
for (const c of collectClassList("deleted_classes")) declared.delete(c);
|
|
185
|
+
const renamed = Array.isArray(m.renamed_classes) ? m.renamed_classes : [];
|
|
186
|
+
for (const r of renamed) {
|
|
187
|
+
if (typeof r !== "object" || r === null) continue;
|
|
188
|
+
const obj = r;
|
|
189
|
+
if (typeof obj.from === "string") declared.delete(obj.from);
|
|
190
|
+
if (typeof obj.to === "string") declared.add(obj.to);
|
|
191
|
+
}
|
|
192
|
+
const transferred = Array.isArray(m.transferred_classes) ? m.transferred_classes : [];
|
|
193
|
+
for (const t of transferred) {
|
|
194
|
+
if (typeof t !== "object" || t === null) continue;
|
|
195
|
+
const obj = t;
|
|
196
|
+
if (typeof obj.to === "string") declared.add(obj.to);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const additions = [];
|
|
200
|
+
for (const c of [...currentClasses].sort()) if (!declared.has(c)) additions.push({
|
|
201
|
+
tag: `flue-class-${c}`,
|
|
202
|
+
new_sqlite_classes: [c]
|
|
203
|
+
});
|
|
204
|
+
return additions;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
130
207
|
* Produce the merged wrangler config: start from the user's, layer Flue's
|
|
131
208
|
* contributions on top. Pure function — caller handles reading and writing.
|
|
132
209
|
*/
|
|
@@ -149,7 +226,10 @@ function mergeFlueAdditions(userConfig, additions) {
|
|
|
149
226
|
const existingMigrations = Array.isArray(merged.migrations) ? merged.migrations : [];
|
|
150
227
|
const existingMigrationTags = new Set(existingMigrations.filter((m) => typeof m === "object" && m !== null).map((m) => m.tag).filter((t) => typeof t === "string"));
|
|
151
228
|
const migrationsOut = [...existingMigrations];
|
|
152
|
-
if (!existingMigrationTags.has(
|
|
229
|
+
for (const migration of additions.migrations) if (!existingMigrationTags.has(migration.tag)) {
|
|
230
|
+
migrationsOut.push(migration);
|
|
231
|
+
existingMigrationTags.add(migration.tag);
|
|
232
|
+
}
|
|
153
233
|
merged.migrations = migrationsOut;
|
|
154
234
|
return merged;
|
|
155
235
|
}
|
|
@@ -319,6 +399,7 @@ var CloudflarePlugin = class {
|
|
|
319
399
|
return `
|
|
320
400
|
// Auto-generated by @flue/sdk build (cloudflare)
|
|
321
401
|
import { Agent, routeAgentRequest } from 'agents';
|
|
402
|
+
import { DurableObject } from 'cloudflare:workers';
|
|
322
403
|
import { Bash, InMemoryFs } from 'just-bash';
|
|
323
404
|
import {
|
|
324
405
|
createFlueContext,
|
|
@@ -371,17 +452,28 @@ async function createLocalEnv() {
|
|
|
371
452
|
|
|
372
453
|
/**
|
|
373
454
|
* Detect and wrap external sandbox instances (e.g. from @cloudflare/sandbox's
|
|
374
|
-
* getSandbox()). Returns SessionEnv if the
|
|
375
|
-
*
|
|
455
|
+
* getSandbox()). Returns SessionEnv if the value looks like a Durable Object
|
|
456
|
+
* RPC stub, null otherwise.
|
|
457
|
+
*
|
|
458
|
+
* NOTE on detection: The value returned by \`getSandbox()\` is a workerd RPC
|
|
459
|
+
* Proxy. \`in\` and \`typeof\` against it return \`true\`/\`'function'\` for any
|
|
460
|
+
* property name, so structural duck-typing is unreliable.
|
|
461
|
+
*
|
|
462
|
+
* \`instanceof <UserSandboxClass>\` ALSO does not work: the RPC stub's
|
|
463
|
+
* prototype chain is workerd's internal \`DurableObject\` runtime class, not
|
|
464
|
+
* the user-defined \`Sandbox\` class (the user's class only exists on the
|
|
465
|
+
* in-DO side; the caller side gets a generic stub). Empirically:
|
|
466
|
+
* typeof stub === 'object'
|
|
467
|
+
* Object.getPrototypeOf(stub).constructor.name === 'DurableObject'
|
|
468
|
+
*
|
|
469
|
+
* \`instanceof DurableObject\` (imported from \`cloudflare:workers\`) is the
|
|
470
|
+
* one signal that holds: it walks the prototype chain via the runtime and
|
|
471
|
+
* matches any DO RPC stub. We treat any DO stub passed to \`init({ sandbox })\`
|
|
472
|
+
* as intended for \`@cloudflare/sandbox\`, since that's the only documented
|
|
473
|
+
* use case for that argument shape on the Cloudflare target.
|
|
376
474
|
*/
|
|
377
475
|
function resolveSandbox(sandbox) {
|
|
378
|
-
if (
|
|
379
|
-
sandbox && typeof sandbox === 'object' &&
|
|
380
|
-
typeof sandbox.exec === 'function' &&
|
|
381
|
-
typeof sandbox.readFile === 'function' &&
|
|
382
|
-
typeof sandbox.destroy === 'function' &&
|
|
383
|
-
!('getCwd' in sandbox) && !('fs' in sandbox)
|
|
384
|
-
) {
|
|
476
|
+
if (sandbox instanceof DurableObject) {
|
|
385
477
|
return cfSandboxToSessionEnv(sandbox);
|
|
386
478
|
}
|
|
387
479
|
return null;
|
|
@@ -652,18 +744,16 @@ export default {
|
|
|
652
744
|
name: agentClassName(a.name)
|
|
653
745
|
}));
|
|
654
746
|
const flueSqliteClasses = flueBindings.map((b) => b.class_name);
|
|
747
|
+
const { config: userConfig, path: userConfigPath } = await this.getUserConfig(ctx.outputDir);
|
|
748
|
+
if (userConfigPath) console.log(`[flue] Merging with user wrangler config: ${userConfigPath}`);
|
|
749
|
+
validateUserWranglerConfig(userConfig);
|
|
750
|
+
const flueMigrations = computeFlueMigrations(flueSqliteClasses, userConfig.migrations);
|
|
655
751
|
const additions = {
|
|
656
752
|
defaultName: path.basename(ctx.outputDir) || "flue-agents",
|
|
657
753
|
main: "_entry.ts",
|
|
658
754
|
doBindings: flueBindings,
|
|
659
|
-
|
|
660
|
-
tag: "flue-v1",
|
|
661
|
-
new_sqlite_classes: flueSqliteClasses
|
|
662
|
-
}
|
|
755
|
+
migrations: flueMigrations
|
|
663
756
|
};
|
|
664
|
-
const { config: userConfig, path: userConfigPath } = await this.getUserConfig(ctx.outputDir);
|
|
665
|
-
if (userConfigPath) console.log(`[flue] Merging with user wrangler config: ${userConfigPath}`);
|
|
666
|
-
validateUserWranglerConfig(userConfig);
|
|
667
757
|
const sandboxClassNames = detectSandboxBindings(userConfig);
|
|
668
758
|
if (sandboxClassNames.length > 0) {
|
|
669
759
|
assertSandboxPackageInstalled(sandboxClassNames, [ctx.outputDir, ctx.workspaceDir]);
|