cursedbelt-server 4.18.0 → 4.18.1
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/server/d1/pullD1.js +16 -3
- package/package.json +1 -1
- package/src/server/d1/pullD1.spec.ts +20 -0
- package/src/server/d1/pullD1.ts +18 -2
package/dist/server/d1/pullD1.js
CHANGED
|
@@ -82,6 +82,8 @@ export async function servedRuntime(url, { attempts = 3, sleep = (ms) => new Pro
|
|
|
82
82
|
}
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
|
+
/** How many times one table's export is asked for when wrangler says success and writes nothing. */
|
|
86
|
+
const EXPORT_ATTEMPTS = 3;
|
|
85
87
|
/**
|
|
86
88
|
* The tables a snapshot carries, from D1's own `sqlite_master` rows — every real table, and
|
|
87
89
|
* none of what cannot or must not be re-inserted: a virtual table and its shadows (`_config`,
|
|
@@ -123,12 +125,23 @@ export async function pullD1ToSqlite(options) {
|
|
|
123
125
|
let sql = '';
|
|
124
126
|
for (const table of tables) {
|
|
125
127
|
const dump = `${destination}.${table}.sql`;
|
|
126
|
-
|
|
128
|
+
// 🔴 `wrangler d1 export` has been measured answering SUCCESS and writing no file —
|
|
129
|
+
// collections' nightly of 2026-09-23T15:35Z refused on `derivative_retries`, and the same
|
|
130
|
+
// command wrote the file (32 bytes, an empty table) an hour later. So a missing file is
|
|
131
|
+
// asked for again, twice, before it is a refusal; a failure that persists still refuses,
|
|
132
|
+
// because a snapshot missing a table is not a backup.
|
|
133
|
+
let exported = { code: 1, stdout: '', stderr: 'not attempted' };
|
|
134
|
+
for (let attempt = 1; attempt <= EXPORT_ATTEMPTS; attempt++) {
|
|
135
|
+
exported = wrangler(['d1', 'export', database, '--remote', '--env=', '--table', table, '--no-schema', '--output', dump, '-y']);
|
|
136
|
+
if (exported.code !== 0 || existsSync(dump))
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
127
139
|
if (exported.code !== 0) {
|
|
128
140
|
throw new Error(`wrangler d1 export --table ${table} failed: ${exported.stderr.trim() || exported.stdout.trim()}`);
|
|
129
141
|
}
|
|
130
|
-
if (!existsSync(dump))
|
|
131
|
-
throw new Error(`wrangler d1 export --table ${table} reported success and wrote no file at ${dump}`);
|
|
142
|
+
if (!existsSync(dump)) {
|
|
143
|
+
throw new Error(`wrangler d1 export --table ${table} reported success ${EXPORT_ATTEMPTS} times and wrote no file at ${dump}`);
|
|
144
|
+
}
|
|
132
145
|
sql += `${readFileSync(dump, 'utf8')}\n`;
|
|
133
146
|
}
|
|
134
147
|
if (!/INSERT INTO/i.test(sql)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt-server",
|
|
3
|
-
"version": "4.18.
|
|
3
|
+
"version": "4.18.1",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The app-facing Bun/Hono server tier of the cursedbelt split — storage, sharing, activity, guard, sync. React-free; cursedbelt-core below it.",
|
|
@@ -172,6 +172,26 @@ describe('pulling D1 down — table by table, because a whole-database export re
|
|
|
172
172
|
).rejects.toThrow(/lists no `tracks` table/);
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
+
test('an export that says success and writes NO file is asked again — and refuses if it never does', async () => {
|
|
176
|
+
const dir = scratch();
|
|
177
|
+
let flaky = 1;
|
|
178
|
+
const run: Wrangler = (args) => {
|
|
179
|
+
if (args[1] === 'execute') return { code: 0, stdout: JSON.stringify([{ results: MASTER }]), stderr: '' };
|
|
180
|
+
const table = args[args.indexOf('--table') + 1] as string;
|
|
181
|
+
const out = args[args.indexOf('--output') + 1] as string;
|
|
182
|
+
// `meta` "succeeds" once without writing — measured on collections, 2026-09-23.
|
|
183
|
+
if (table === 'meta' && flaky-- > 0) return { code: 0, stdout: 'Downloaded successfully', stderr: '' };
|
|
184
|
+
writeFileSync(out, table === 'items' ? "INSERT INTO items (id) VALUES ('i1');" : '');
|
|
185
|
+
return { code: 0, stdout: '', stderr: '' };
|
|
186
|
+
};
|
|
187
|
+
await pull(dir, run);
|
|
188
|
+
const never: Wrangler = (args) =>
|
|
189
|
+
args[1] === 'execute'
|
|
190
|
+
? { code: 0, stdout: JSON.stringify([{ results: MASTER }]), stderr: '' }
|
|
191
|
+
: { code: 0, stdout: 'Downloaded successfully', stderr: '' };
|
|
192
|
+
await expect(pull(scratch(), never)).rejects.toThrow(/reported success 3 times and wrote no file/);
|
|
193
|
+
});
|
|
194
|
+
|
|
175
195
|
test('🔴 one table failing to export THROWS — never a snapshot missing that table', async () => {
|
|
176
196
|
const dir = scratch();
|
|
177
197
|
const { run } = fake((table) => (table === 'meta' ? null : "INSERT INTO items (id) VALUES ('x');"));
|
package/src/server/d1/pullD1.ts
CHANGED
|
@@ -97,6 +97,9 @@ export async function servedRuntime(
|
|
|
97
97
|
return null;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/** How many times one table's export is asked for when wrangler says success and writes nothing. */
|
|
101
|
+
const EXPORT_ATTEMPTS = 3;
|
|
102
|
+
|
|
100
103
|
/** One `wrangler` invocation, as {@link pullD1ToSqlite} needs it. Injected so a spec runs none. */
|
|
101
104
|
export type Wrangler = (args: string[]) => { code: number; stdout: string; stderr: string };
|
|
102
105
|
|
|
@@ -160,11 +163,24 @@ export async function pullD1ToSqlite(options: PullD1Options): Promise<void> {
|
|
|
160
163
|
let sql = '';
|
|
161
164
|
for (const table of tables) {
|
|
162
165
|
const dump = `${destination}.${table}.sql`;
|
|
163
|
-
|
|
166
|
+
// 🔴 `wrangler d1 export` has been measured answering SUCCESS and writing no file —
|
|
167
|
+
// collections' nightly of 2026-09-23T15:35Z refused on `derivative_retries`, and the same
|
|
168
|
+
// command wrote the file (32 bytes, an empty table) an hour later. So a missing file is
|
|
169
|
+
// asked for again, twice, before it is a refusal; a failure that persists still refuses,
|
|
170
|
+
// because a snapshot missing a table is not a backup.
|
|
171
|
+
let exported = { code: 1, stdout: '', stderr: 'not attempted' };
|
|
172
|
+
for (let attempt = 1; attempt <= EXPORT_ATTEMPTS; attempt++) {
|
|
173
|
+
exported = wrangler(['d1', 'export', database, '--remote', '--env=', '--table', table, '--no-schema', '--output', dump, '-y']);
|
|
174
|
+
if (exported.code !== 0 || existsSync(dump)) break;
|
|
175
|
+
}
|
|
164
176
|
if (exported.code !== 0) {
|
|
165
177
|
throw new Error(`wrangler d1 export --table ${table} failed: ${exported.stderr.trim() || exported.stdout.trim()}`);
|
|
166
178
|
}
|
|
167
|
-
if (!existsSync(dump))
|
|
179
|
+
if (!existsSync(dump)) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`wrangler d1 export --table ${table} reported success ${EXPORT_ATTEMPTS} times and wrote no file at ${dump}`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
168
184
|
sql += `${readFileSync(dump, 'utf8')}\n`;
|
|
169
185
|
}
|
|
170
186
|
if (!/INSERT INTO/i.test(sql)) {
|