@auersperg/flare 0.3.8 → 0.3.10
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/README.md +3 -1
- package/dist/cli/{chunk-5s6zv1h9.js → chunk-jrptpjmm.js} +1 -1
- package/dist/cli/{chunk-y6e2h40n.js → chunk-m0698kap.js} +37 -12
- package/dist/cli/{chunk-5yg1zj5n.js → chunk-znktbexz.js} +6 -6
- package/dist/cli/{chunk-vx7kmczm.js → chunk-zt96ghm7.js} +12 -6
- package/dist/cli/flare.js +5 -5
- package/dist/platform/admin.js +31 -23
- package/dist/platform/dashboard/_astro/{auth-view.EASEF9BN.js → auth-view.BhUFPmNv.js} +1 -1
- package/dist/platform/dashboard/_astro/{cli-approval.DwZVHdyo.js → cli-approval.emPTVRqn.js} +1 -1
- package/dist/platform/dashboard/_astro/client.4z54xJ_z.js +1 -0
- package/dist/platform/dashboard/_astro/{common.TAClb6G9.js → common.9pujiaSZ.js} +1 -1
- package/dist/platform/dashboard/_astro/{data-view.BaHEU4Xa.js → data-view.C-VU8Oxm.js} +1 -1
- package/dist/platform/dashboard/_astro/{functions-view.BGOYuDwu.js → functions-view.3yLrtS0h.js} +1 -1
- package/dist/platform/dashboard/_astro/{index.astro_astro_type_script_index_0_lang.Blg5hU9g.js → index.astro_astro_type_script_index_0_lang.Dsfj1oeZ.js} +2 -2
- package/dist/platform/dashboard/_astro/{input-group.DUp-h0xx.js → input-group.BZ1TZT7L.js} +1 -1
- package/dist/platform/dashboard/_astro/{sdk-view.2ErPn4cO.js → sdk-view.BAiQKNm8.js} +1 -1
- package/dist/platform/dashboard/_astro/{services-view.iZujitxl.js → services-view.BSrqpyr5.js} +1 -1
- package/dist/platform/dashboard/_astro/{settings-view.CqK-mrlB.js → settings-view.C_eYjlIE.js} +1 -1
- package/dist/platform/dashboard/bootstrap/facet.js +64 -46
- package/dist/platform/dashboard/bootstrap/openapi.json +1 -1
- package/dist/platform/dashboard/bootstrap/release.json +2 -2
- package/dist/platform/dashboard/index.html +1 -1
- package/dist/platform/manifest.json +36 -36
- package/dist/platform/runtime.js +99 -66
- package/dist/sdk/backup.js +4 -4
- package/dist/sdk/client.js +22 -41
- package/dist/sdk/cloudflare.js +10 -11
- package/dist/sdk/database.js +45 -27
- package/dist/sdk/http.d.ts +2 -0
- package/dist/sdk/http.js +27 -0
- package/dist/sdk/schema.d.ts +2 -2
- package/dist/sdk/schema.js +7 -6
- package/package.json +2 -2
- package/dist/platform/dashboard/_astro/client.BUUZgEFj.js +0 -1
package/dist/platform/runtime.js
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
import { Buffer as Buffer2 } from "node:buffer";
|
|
3
3
|
import { scrypt, timingSafeEqual } from "node:crypto";
|
|
4
4
|
|
|
5
|
+
// packages/flare/src/sdk/http.ts
|
|
6
|
+
async function readBody(body, limit, tooLarge) {
|
|
7
|
+
const reader = body.getReader();
|
|
8
|
+
const chunks = [];
|
|
9
|
+
let size = 0;
|
|
10
|
+
try {
|
|
11
|
+
for (;; ) {
|
|
12
|
+
const { done, value } = await reader.read();
|
|
13
|
+
if (done)
|
|
14
|
+
break;
|
|
15
|
+
size += value.byteLength;
|
|
16
|
+
if (size > limit)
|
|
17
|
+
tooLarge();
|
|
18
|
+
chunks.push(value);
|
|
19
|
+
}
|
|
20
|
+
} finally {
|
|
21
|
+
await reader.cancel().catch(() => {});
|
|
22
|
+
}
|
|
23
|
+
if (chunks.length === 1)
|
|
24
|
+
return chunks[0];
|
|
25
|
+
const bytes = new Uint8Array(size);
|
|
26
|
+
let offset = 0;
|
|
27
|
+
for (const chunk of chunks) {
|
|
28
|
+
bytes.set(chunk, offset);
|
|
29
|
+
offset += chunk.byteLength;
|
|
30
|
+
}
|
|
31
|
+
return bytes;
|
|
32
|
+
}
|
|
33
|
+
|
|
5
34
|
// packages/flare/src/sdk/protocol.ts
|
|
6
35
|
var LIMITS = Object.freeze({
|
|
7
36
|
inputBytes: 65536,
|
|
@@ -84,28 +113,7 @@ async function jsonBody(request, limit = LIMITS.inputBytes) {
|
|
|
84
113
|
fail("INVALID_CONTENT_TYPE");
|
|
85
114
|
if (!request.body)
|
|
86
115
|
fail("INVALID_INPUT");
|
|
87
|
-
const
|
|
88
|
-
const chunks = [];
|
|
89
|
-
let size = 0;
|
|
90
|
-
try {
|
|
91
|
-
for (;; ) {
|
|
92
|
-
const { done, value } = await reader.read();
|
|
93
|
-
if (done)
|
|
94
|
-
break;
|
|
95
|
-
size += value.byteLength;
|
|
96
|
-
if (size > limit)
|
|
97
|
-
fail("TOO_LARGE");
|
|
98
|
-
chunks.push(value);
|
|
99
|
-
}
|
|
100
|
-
} finally {
|
|
101
|
-
await reader.cancel().catch(() => {});
|
|
102
|
-
}
|
|
103
|
-
const bytes = new Uint8Array(size);
|
|
104
|
-
let offset = 0;
|
|
105
|
-
for (const c of chunks) {
|
|
106
|
-
bytes.set(c, offset);
|
|
107
|
-
offset += c.byteLength;
|
|
108
|
-
}
|
|
116
|
+
const bytes = await readBody(request.body, limit, () => fail("TOO_LARGE"));
|
|
109
117
|
try {
|
|
110
118
|
const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
111
119
|
boundedJson(parsed, limit);
|
|
@@ -291,6 +299,12 @@ function validateAuthBackup(users) {
|
|
|
291
299
|
var ttl = 12 * 60 * 60 * 1000;
|
|
292
300
|
var kdf = { N: 16384, r: 8, p: 5, maxmem: 32 * 1024 * 1024 };
|
|
293
301
|
var dummySalt = "00000000000000000000000000000000";
|
|
302
|
+
var userInfo = (row) => ({
|
|
303
|
+
id: String(row.id),
|
|
304
|
+
login: String(row.login),
|
|
305
|
+
disabled: !!row.disabled,
|
|
306
|
+
createdAt: Number(row.createdAt)
|
|
307
|
+
});
|
|
294
308
|
function derive(password, salt) {
|
|
295
309
|
return new Promise((resolve, reject) => scrypt(password, salt, 32, kdf, (error, value) => error ? reject(error) : resolve(value)));
|
|
296
310
|
}
|
|
@@ -405,12 +419,7 @@ class PasswordAuth {
|
|
|
405
419
|
const row = this.storage.sql.exec("SELECT id,login,disabled,createdAt FROM auth_users WHERE id=?", id).toArray()[0];
|
|
406
420
|
if (!row)
|
|
407
421
|
fail("NOT_FOUND");
|
|
408
|
-
return
|
|
409
|
-
id: String(row.id),
|
|
410
|
-
login: String(row.login),
|
|
411
|
-
disabled: !!row.disabled,
|
|
412
|
-
createdAt: Number(row.createdAt)
|
|
413
|
-
};
|
|
422
|
+
return userInfo(row);
|
|
414
423
|
}
|
|
415
424
|
async authenticate(token) {
|
|
416
425
|
if (!/^flared_session_[a-f0-9]{64}$/.test(token) || !this.settings().enabled)
|
|
@@ -498,9 +507,9 @@ class PasswordAuth {
|
|
|
498
507
|
const cursor = input.cursor ?? "";
|
|
499
508
|
if (typeof cursor !== "string" || cursor.length > 36)
|
|
500
509
|
fail("INVALID_CURSOR");
|
|
501
|
-
const rows = this.storage.sql.exec("SELECT id FROM auth_users WHERE id>? ORDER BY id LIMIT 51", cursor).toArray();
|
|
510
|
+
const rows = this.storage.sql.exec("SELECT id,login,disabled,createdAt FROM auth_users WHERE id>? ORDER BY id LIMIT 51", cursor).toArray();
|
|
502
511
|
return {
|
|
503
|
-
users: rows.slice(0, 50).map(
|
|
512
|
+
users: rows.slice(0, 50).map(userInfo),
|
|
504
513
|
next: rows.length > 50 ? String(rows[49].id) : null
|
|
505
514
|
};
|
|
506
515
|
}
|
|
@@ -560,7 +569,7 @@ function dashboardIdentity(value) {
|
|
|
560
569
|
// packages/core/package.json
|
|
561
570
|
var package_default = {
|
|
562
571
|
name: "@flare/platform",
|
|
563
|
-
version: "0.3.
|
|
572
|
+
version: "0.3.10",
|
|
564
573
|
type: "module",
|
|
565
574
|
license: "MIT",
|
|
566
575
|
private: true,
|
|
@@ -901,6 +910,25 @@ function buildKey(scope, hash, role) {
|
|
|
901
910
|
var BACKUP_BYTES = SNAPSHOT_BYTES + 4 * 1048576;
|
|
902
911
|
var BACKUP_FILES = 100;
|
|
903
912
|
var BACKUP_FILE_BYTES = 100 * 1048576;
|
|
913
|
+
async function copyInParallel(files, copy) {
|
|
914
|
+
const copied = new Array(files.length);
|
|
915
|
+
let next = 0;
|
|
916
|
+
const results = await Promise.allSettled(Array.from({ length: Math.min(3, files.length) }, async () => {
|
|
917
|
+
while (next < files.length) {
|
|
918
|
+
const i = next++;
|
|
919
|
+
try {
|
|
920
|
+
copied[i] = await copy(files[i]);
|
|
921
|
+
} catch (error) {
|
|
922
|
+
next = files.length;
|
|
923
|
+
throw error;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}));
|
|
927
|
+
for (const result of results)
|
|
928
|
+
if (result.status === "rejected")
|
|
929
|
+
throw result.reason;
|
|
930
|
+
return copied;
|
|
931
|
+
}
|
|
904
932
|
function backupPrefix(scope, id) {
|
|
905
933
|
return `backups/${scopeForDeletion(scope)}/${id === undefined ? "" : uuid(id) + "/"}`;
|
|
906
934
|
}
|
|
@@ -955,8 +983,8 @@ class BackupStore {
|
|
|
955
983
|
fail("BACKUP_TOO_LARGE");
|
|
956
984
|
if (files.length && !this.files)
|
|
957
985
|
fail("UNSUPPORTED");
|
|
958
|
-
const prefix = backupPrefix(value.scope, value.id)
|
|
959
|
-
|
|
986
|
+
const prefix = backupPrefix(value.scope, value.id);
|
|
987
|
+
const copied = await copyInParallel(files, async (file) => {
|
|
960
988
|
const object = await this.files.get(fileKey(value.scope, file.id));
|
|
961
989
|
if (!object || object.size !== file.size || !/^[a-f0-9]{32}$/.test(object.etag))
|
|
962
990
|
fail("INVALID_BACKUP");
|
|
@@ -966,8 +994,8 @@ class BackupStore {
|
|
|
966
994
|
});
|
|
967
995
|
if (!stored || stored.size !== file.size || stored.etag !== object.etag)
|
|
968
996
|
fail("INVALID_BACKUP");
|
|
969
|
-
|
|
970
|
-
}
|
|
997
|
+
return { ...file, md5: stored.etag };
|
|
998
|
+
});
|
|
971
999
|
const backup = { ...value, files: copied }, encoded = boundedJson(backup, BACKUP_BYTES);
|
|
972
1000
|
const stored = await this.bucket.put(prefix + "manifest.json", encoded, {
|
|
973
1001
|
onlyIf: { etagDoesNotMatch: "*" },
|
|
@@ -1005,11 +1033,10 @@ class BackupStore {
|
|
|
1005
1033
|
limit: 20,
|
|
1006
1034
|
cursor
|
|
1007
1035
|
});
|
|
1008
|
-
const backups =
|
|
1009
|
-
for (const prefix of page.delimitedPrefixes) {
|
|
1036
|
+
const backups = await Promise.all(page.delimitedPrefixes.map(async (prefix) => {
|
|
1010
1037
|
const id = prefix.slice(backupPrefix(scope).length, -1);
|
|
1011
1038
|
const manifest = await this.bucket.head(prefix + "manifest.json"), meta = manifest?.customMetadata;
|
|
1012
|
-
|
|
1039
|
+
return meta ? {
|
|
1013
1040
|
id,
|
|
1014
1041
|
scope,
|
|
1015
1042
|
at: Number(meta.at),
|
|
@@ -1017,14 +1044,14 @@ class BackupStore {
|
|
|
1017
1044
|
revision: Number(meta.revision),
|
|
1018
1045
|
rows: Number(meta.rows),
|
|
1019
1046
|
files: Number(meta.files)
|
|
1020
|
-
} : { id, scope, state: "incomplete" }
|
|
1021
|
-
}
|
|
1047
|
+
} : { id, scope, state: "incomplete" };
|
|
1048
|
+
}));
|
|
1022
1049
|
return { page: backups, cursor: page.truncated ? page.cursor : null };
|
|
1023
1050
|
}
|
|
1024
1051
|
async copyFiles(value, target) {
|
|
1025
1052
|
if (value.files.length && !this.files)
|
|
1026
1053
|
fail("UNSUPPORTED");
|
|
1027
|
-
|
|
1054
|
+
await copyInParallel(value.files, async (file) => {
|
|
1028
1055
|
const object = await this.bucket.get(backupPrefix(value.scope, value.id) + "files/" + file.id, { onlyIf: { etagMatches: file.md5 } });
|
|
1029
1056
|
if (!object || !("body" in object) || !object.body || object.size !== file.size)
|
|
1030
1057
|
fail("INVALID_BACKUP");
|
|
@@ -1034,7 +1061,7 @@ class BackupStore {
|
|
|
1034
1061
|
});
|
|
1035
1062
|
if (!stored || stored.size !== file.size || stored.etag !== file.md5)
|
|
1036
1063
|
fail("INVALID_BACKUP");
|
|
1037
|
-
}
|
|
1064
|
+
});
|
|
1038
1065
|
}
|
|
1039
1066
|
async remove(scope, id) {
|
|
1040
1067
|
const prefix = backupPrefix(scope, id);
|
|
@@ -1097,7 +1124,7 @@ class BackupInventory {
|
|
|
1097
1124
|
await this.storage.sync();
|
|
1098
1125
|
}
|
|
1099
1126
|
const complete = !!this.storage.sql.exec("SELECT complete FROM backup_inventory WHERE id=1").one().complete;
|
|
1100
|
-
const rows = before ? this.storage.sql.exec("SELECT * FROM backup_index WHERE
|
|
1127
|
+
const rows = before ? this.storage.sql.exec("SELECT * FROM backup_index WHERE (at,id)<(?,?) ORDER BY at DESC,id DESC LIMIT 21", before.at, before.id).toArray() : this.storage.sql.exec("SELECT * FROM backup_index ORDER BY at DESC,id DESC LIMIT 21").toArray();
|
|
1101
1128
|
const page = rows.slice(0, 20).map((r) => JSON.parse(String(r.summary)));
|
|
1102
1129
|
const last = rows[19];
|
|
1103
1130
|
const newest = complete ? this.storage.sql.exec("SELECT summary FROM backup_index WHERE at>=0 ORDER BY at DESC,id DESC LIMIT 1").toArray()[0] : undefined;
|
|
@@ -1185,8 +1212,10 @@ function queueBatches(bodies) {
|
|
|
1185
1212
|
return batches;
|
|
1186
1213
|
}
|
|
1187
1214
|
async function consumeBatch(batch, work, failed) {
|
|
1188
|
-
|
|
1189
|
-
|
|
1215
|
+
let next = 0;
|
|
1216
|
+
await Promise.all(Array.from({ length: Math.min(4, batch.messages.length) }, async () => {
|
|
1217
|
+
while (next < batch.messages.length) {
|
|
1218
|
+
const message = batch.messages[next++];
|
|
1190
1219
|
try {
|
|
1191
1220
|
await work(message.body);
|
|
1192
1221
|
message.ack();
|
|
@@ -1194,8 +1223,8 @@ async function consumeBatch(batch, work, failed) {
|
|
|
1194
1223
|
failed(message.body, error);
|
|
1195
1224
|
message.retry();
|
|
1196
1225
|
}
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1226
|
+
}
|
|
1227
|
+
}));
|
|
1199
1228
|
}
|
|
1200
1229
|
|
|
1201
1230
|
// packages/core/src/capabilities.ts
|
|
@@ -8110,6 +8139,7 @@ class EnvironmentSupervisor extends DurableObject {
|
|
|
8110
8139
|
dueAt: Date.now()
|
|
8111
8140
|
});
|
|
8112
8141
|
await this.recover();
|
|
8142
|
+
await this.dashboardHint();
|
|
8113
8143
|
})
|
|
8114
8144
|
}
|
|
8115
8145
|
});
|
|
@@ -8141,6 +8171,7 @@ class EnvironmentSupervisor extends DurableObject {
|
|
|
8141
8171
|
this.ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS retention (id INTEGER PRIMARY KEY,next INTEGER NOT NULL)");
|
|
8142
8172
|
this.ctx.storage.sql.exec("CREATE INDEX IF NOT EXISTS receipts_at ON receipts(at)");
|
|
8143
8173
|
this.ctx.storage.sql.exec("CREATE INDEX IF NOT EXISTS job_events_retention ON job_events(state,at)");
|
|
8174
|
+
this.ctx.storage.sql.exec("CREATE INDEX IF NOT EXISTS job_events_at ON job_events(at)");
|
|
8144
8175
|
this.ctx.storage.sql.exec("CREATE INDEX IF NOT EXISTS job_events_release ON job_events(release,state)");
|
|
8145
8176
|
this.nextRetention = Number(this.ctx.storage.sql.exec("SELECT next FROM retention WHERE id=1").toArray()[0]?.next ?? 0);
|
|
8146
8177
|
const alive = new Set([...this.sockets.getConnections()].map((c) => c.id));
|
|
@@ -8427,30 +8458,28 @@ class EnvironmentSupervisor extends DurableObject {
|
|
|
8427
8458
|
return s.auth;
|
|
8428
8459
|
}
|
|
8429
8460
|
async snapshot(c, row, cache = new SnapshotCache) {
|
|
8430
|
-
const auth = this.connectionAuth(c)
|
|
8461
|
+
const auth = this.connectionAuth(c);
|
|
8431
8462
|
if (Number(row.sent) > Number(row.acked)) {
|
|
8432
8463
|
this.ctx.storage.sql.exec("UPDATE subscriptions SET pending=1 WHERE connection=? AND id=?", c.id, row.id);
|
|
8433
8464
|
return;
|
|
8434
8465
|
}
|
|
8435
8466
|
try {
|
|
8467
|
+
const args = JSON.parse(row.args), key = stableJson({ path: row.path, args, auth });
|
|
8436
8468
|
const { value: result, hash } = await cache.get(key, () => this.invoke({
|
|
8437
8469
|
path: row.path,
|
|
8438
8470
|
kind: "query",
|
|
8439
|
-
args
|
|
8471
|
+
args,
|
|
8440
8472
|
auth
|
|
8441
8473
|
}));
|
|
8442
|
-
|
|
8443
|
-
if (
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
revision: result.revision
|
|
8452
|
-
}, LIMITS.outputBytes + 1024));
|
|
8453
|
-
this.ctx.storage.sql.exec("UPDATE subscriptions SET sent=?,hash=?,pending=0 WHERE connection=? AND id=?", result.revision, hash, c.id, row.id);
|
|
8474
|
+
const changed = row.hash !== hash;
|
|
8475
|
+
if (changed)
|
|
8476
|
+
c.send(boundedJson({
|
|
8477
|
+
type: "snapshot",
|
|
8478
|
+
id: row.id,
|
|
8479
|
+
result: result.result,
|
|
8480
|
+
revision: result.revision
|
|
8481
|
+
}, LIMITS.outputBytes + 1024));
|
|
8482
|
+
this.ctx.storage.sql.exec("UPDATE subscriptions SET dependencies=?,checked=?,release=?,sent=?,hash=?,pending=0 WHERE connection=? AND id=?", JSON.stringify(result.tables), result.revision, this.current(), changed ? result.revision : row.sent, hash, c.id, row.id);
|
|
8454
8483
|
} catch (e) {
|
|
8455
8484
|
c.send(JSON.stringify({ type: "error", id: row.id, code: status(e).code }));
|
|
8456
8485
|
this.ctx.storage.sql.exec("DELETE FROM subscriptions WHERE connection=? AND id=?", c.id, row.id);
|
|
@@ -8491,7 +8520,6 @@ class EnvironmentSupervisor extends DurableObject {
|
|
|
8491
8520
|
}
|
|
8492
8521
|
}
|
|
8493
8522
|
async broadcast() {
|
|
8494
|
-
await this.dashboardHint();
|
|
8495
8523
|
const connections = [...this.sockets.getConnections()].filter((c) => !c.state?.dashboard);
|
|
8496
8524
|
if (!connections.length)
|
|
8497
8525
|
return;
|
|
@@ -8550,8 +8578,10 @@ class EnvironmentSupervisor extends DurableObject {
|
|
|
8550
8578
|
}
|
|
8551
8579
|
async onJob({ job }) {
|
|
8552
8580
|
await this.serial(async () => {
|
|
8553
|
-
if (job.fn === "recover")
|
|
8581
|
+
if (job.fn === "recover") {
|
|
8554
8582
|
await this.recover();
|
|
8583
|
+
await this.dashboardHint();
|
|
8584
|
+
}
|
|
8555
8585
|
if (job.fn === "files-cleanup")
|
|
8556
8586
|
await this.files().sweep();
|
|
8557
8587
|
if (job.fn === "cron-reconcile")
|
|
@@ -8682,8 +8712,10 @@ class EnvironmentSupervisor extends DurableObject {
|
|
|
8682
8712
|
if (input.authorization && !input.authorization.startsWith("Bearer "))
|
|
8683
8713
|
fail("UNAUTHORIZED");
|
|
8684
8714
|
const result = await this.passwords().public(input.operation, input.body ?? {}, input.authorization?.slice(7) ?? null);
|
|
8685
|
-
|
|
8686
|
-
|
|
8715
|
+
if (input.operation !== "me") {
|
|
8716
|
+
this.closeRevokedSockets();
|
|
8717
|
+
await this.dashboardHint();
|
|
8718
|
+
}
|
|
8687
8719
|
return result;
|
|
8688
8720
|
});
|
|
8689
8721
|
}
|
|
@@ -8948,7 +8980,8 @@ class EnvironmentSupervisor extends DurableObject {
|
|
|
8948
8980
|
if (op.startsWith("auth-")) {
|
|
8949
8981
|
this.assertAvailable();
|
|
8950
8982
|
const result = await this.passwords().admin(op, input);
|
|
8951
|
-
|
|
8983
|
+
if (op !== "auth-users" && op !== "auth-settings")
|
|
8984
|
+
this.closeRevokedSockets();
|
|
8952
8985
|
return result;
|
|
8953
8986
|
}
|
|
8954
8987
|
if (op === "release-export") {
|
package/dist/sdk/backup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { boundedJson, FlareError, stableJson } from "./protocol.js";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
documentJson,
|
|
4
4
|
quote,
|
|
5
5
|
schemaContract
|
|
6
6
|
} from "./schema.js";
|
|
@@ -66,10 +66,10 @@ export async function restoreData(storage, schema, id, snapshot) {
|
|
|
66
66
|
for (const doc of docs) {
|
|
67
67
|
if (++count > SNAPSHOT_ROWS || !doc || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(doc._id) || !Number.isFinite(doc._creationTime) || doc._creationTime < 0)
|
|
68
68
|
reject("INVALID_BACKUP");
|
|
69
|
-
const value = await
|
|
70
|
-
if (
|
|
69
|
+
const value = await documentJson(schema[name], doc.value);
|
|
70
|
+
if (value !== stableJson(doc.value))
|
|
71
71
|
reject("INVALID_BACKUP");
|
|
72
|
-
storage.sql.exec(`INSERT INTO ${quote(name)} VALUES (?,?,?)`, doc._id, doc._creationTime,
|
|
72
|
+
storage.sql.exec(`INSERT INTO ${quote(name)} VALUES (?,?,?)`, doc._id, doc._creationTime, value);
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
storage.sql.exec("UPDATE _flared_revision SET value=? WHERE id=1", snapshot.revision);
|
package/dist/sdk/client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readBody } from "./http.js";
|
|
1
2
|
import {
|
|
2
3
|
assertPath,
|
|
3
4
|
boundedJson,
|
|
@@ -37,12 +38,17 @@ export async function uploadFile(grant, file, options = {}) {
|
|
|
37
38
|
body: file,
|
|
38
39
|
redirect: "error",
|
|
39
40
|
credentials: "omit",
|
|
40
|
-
signal:
|
|
41
|
+
signal: AbortSignal.any([
|
|
42
|
+
...options.signal ? [options.signal] : [],
|
|
43
|
+
AbortSignal.timeout(options.timeoutMs ?? 120000)
|
|
44
|
+
])
|
|
41
45
|
});
|
|
42
46
|
} catch {
|
|
43
47
|
throw new FlareError("UPLOAD_UNKNOWN", "Connection interrupted or upload timed out; refresh the file list before trying again", true);
|
|
44
48
|
}
|
|
45
|
-
const result = await response.
|
|
49
|
+
const result = response.body ? await readBody(response.body, LIMITS.outputBytes, () => {
|
|
50
|
+
throw Error("Upload response exceeds limit");
|
|
51
|
+
}).then((bytes) => JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes))).catch(() => null) : null;
|
|
46
52
|
if (!response.ok) {
|
|
47
53
|
const code = result && typeof result === "object" && "error" in result && typeof result.error === "string" ? result.error : `HTTP_${response.status}`;
|
|
48
54
|
throw new FlareError(code, `Upload failed: ${code}`, response.status >= 500);
|
|
@@ -77,7 +83,7 @@ export function createClient(options) {
|
|
|
77
83
|
const notify = (sub, method, value) => {
|
|
78
84
|
for (const observer of sub.observers) {
|
|
79
85
|
try {
|
|
80
|
-
observer[method]?.(method === "next" ? JSON.parse(
|
|
86
|
+
observer[method]?.(method === "next" ? JSON.parse(value) : value);
|
|
81
87
|
} catch {}
|
|
82
88
|
}
|
|
83
89
|
};
|
|
@@ -110,28 +116,9 @@ export function createClient(options) {
|
|
|
110
116
|
});
|
|
111
117
|
if (!response.body)
|
|
112
118
|
throw new FlareError("INVALID_RESPONSE", "Missing response body", write);
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
for (;; ) {
|
|
118
|
-
const { done, value } = await reader.read();
|
|
119
|
-
if (done)
|
|
120
|
-
break;
|
|
121
|
-
bytes += value.byteLength;
|
|
122
|
-
if (bytes > LIMITS.outputBytes)
|
|
123
|
-
throw new FlareError("INVALID_RESPONSE", "Response exceeds limit", write);
|
|
124
|
-
chunks.push(value);
|
|
125
|
-
}
|
|
126
|
-
} finally {
|
|
127
|
-
await reader.cancel().catch(() => {});
|
|
128
|
-
}
|
|
129
|
-
const combined = new Uint8Array(bytes);
|
|
130
|
-
let offset = 0;
|
|
131
|
-
for (const chunk of chunks) {
|
|
132
|
-
combined.set(chunk, offset);
|
|
133
|
-
offset += chunk.byteLength;
|
|
134
|
-
}
|
|
119
|
+
const combined = await readBody(response.body, LIMITS.outputBytes, () => {
|
|
120
|
+
throw new FlareError("INVALID_RESPONSE", "Response exceeds limit", write);
|
|
121
|
+
});
|
|
135
122
|
let data;
|
|
136
123
|
try {
|
|
137
124
|
data = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(combined));
|
|
@@ -195,7 +182,7 @@ export function createClient(options) {
|
|
|
195
182
|
attempt = 0;
|
|
196
183
|
for (const sub of subscriptions.values()) {
|
|
197
184
|
sub.revision = -1;
|
|
198
|
-
sub.
|
|
185
|
+
sub.snapshot = undefined;
|
|
199
186
|
if (!sub.terminal)
|
|
200
187
|
register(sub);
|
|
201
188
|
}
|
|
@@ -215,8 +202,7 @@ export function createClient(options) {
|
|
|
215
202
|
if (frame.type === "error") {
|
|
216
203
|
sub.terminal = true;
|
|
217
204
|
if (authorizationErrors.has(frame.code)) {
|
|
218
|
-
sub.
|
|
219
|
-
sub.value = undefined;
|
|
205
|
+
sub.snapshot = undefined;
|
|
220
206
|
}
|
|
221
207
|
notify(sub, "error", new FlareError(typeof frame.code === "string" ? frame.code : "LIVE_ERROR", "Live subscription failed"));
|
|
222
208
|
return;
|
|
@@ -225,12 +211,11 @@ export function createClient(options) {
|
|
|
225
211
|
throw new Error("Invalid snapshot");
|
|
226
212
|
const json = boundedJson(frame.result, LIMITS.outputBytes);
|
|
227
213
|
if (frame.revision > sub.revision) {
|
|
228
|
-
const changed =
|
|
214
|
+
const changed = sub.snapshot !== json;
|
|
229
215
|
sub.revision = frame.revision;
|
|
230
|
-
sub.
|
|
231
|
-
sub.hasValue = true;
|
|
216
|
+
sub.snapshot = json;
|
|
232
217
|
if (changed)
|
|
233
|
-
notify(sub, "next", sub.
|
|
218
|
+
notify(sub, "next", sub.snapshot);
|
|
234
219
|
}
|
|
235
220
|
send({ type: "ack", id: sub.id, revision: sub.revision });
|
|
236
221
|
} catch {
|
|
@@ -244,8 +229,7 @@ export function createClient(options) {
|
|
|
244
229
|
socket = undefined;
|
|
245
230
|
for (const sub of subscriptions.values()) {
|
|
246
231
|
if (event.code === 1008) {
|
|
247
|
-
sub.
|
|
248
|
-
sub.value = undefined;
|
|
232
|
+
sub.snapshot = undefined;
|
|
249
233
|
sub.revision = -1;
|
|
250
234
|
}
|
|
251
235
|
if (!sub.terminal)
|
|
@@ -271,8 +255,7 @@ export function createClient(options) {
|
|
|
271
255
|
return;
|
|
272
256
|
for (const sub of subscriptions.values()) {
|
|
273
257
|
if (error instanceof FlareError && authorizationErrors.has(error.code)) {
|
|
274
|
-
sub.
|
|
275
|
-
sub.value = undefined;
|
|
258
|
+
sub.snapshot = undefined;
|
|
276
259
|
sub.revision = -1;
|
|
277
260
|
}
|
|
278
261
|
notify(sub, "error", error instanceof FlareError ? error : new FlareError("LIVE_UNAVAILABLE", "Live connection failed"));
|
|
@@ -319,7 +302,6 @@ export function createClient(options) {
|
|
|
319
302
|
args: JSON.parse(stableJson(args)),
|
|
320
303
|
observers: new Set,
|
|
321
304
|
revision: -1,
|
|
322
|
-
hasValue: false,
|
|
323
305
|
terminal: false
|
|
324
306
|
};
|
|
325
307
|
subscriptions.set(key, sub);
|
|
@@ -328,8 +310,8 @@ export function createClient(options) {
|
|
|
328
310
|
}
|
|
329
311
|
const registration = { next: observer.next, error: observer.error };
|
|
330
312
|
sub.observers.add(registration);
|
|
331
|
-
if (sub.
|
|
332
|
-
notify({ ...sub, observers: new Set([observer]) }, "next", sub.
|
|
313
|
+
if (sub.snapshot !== undefined)
|
|
314
|
+
notify({ ...sub, observers: new Set([observer]) }, "next", sub.snapshot);
|
|
333
315
|
connect();
|
|
334
316
|
let unsubscribed = false;
|
|
335
317
|
return () => {
|
|
@@ -367,8 +349,7 @@ export function createClient(options) {
|
|
|
367
349
|
socket = undefined;
|
|
368
350
|
old?.close(1000, "Authorization changed");
|
|
369
351
|
for (const sub of subscriptions.values()) {
|
|
370
|
-
sub.
|
|
371
|
-
sub.value = undefined;
|
|
352
|
+
sub.snapshot = undefined;
|
|
372
353
|
sub.revision = -1;
|
|
373
354
|
sub.terminal = false;
|
|
374
355
|
notify(sub, "error", new FlareError("AUTH_CHANGED", "Authorization changed; cached value invalidated"));
|
package/dist/sdk/cloudflare.js
CHANGED
|
@@ -102,7 +102,7 @@ export function createFacet(app) {
|
|
|
102
102
|
}
|
|
103
103
|
let result = null;
|
|
104
104
|
const changed = new Set;
|
|
105
|
-
await this.ctx.storage.transaction(async () => {
|
|
105
|
+
const revision = await this.ctx.storage.transaction(async () => {
|
|
106
106
|
let active = true;
|
|
107
107
|
const scheduled = [];
|
|
108
108
|
const jobs = Object.freeze({
|
|
@@ -136,9 +136,7 @@ export function createFacet(app) {
|
|
|
136
136
|
if (!active)
|
|
137
137
|
throw new FlareError("CAPABILITY_EXPIRED", "Mutation invocation ended");
|
|
138
138
|
const id = crypto.randomUUID();
|
|
139
|
-
this.ctx.storage.sql.exec("INSERT INTO _flared_outbox (id,release,path,args,auth,dueAt) VALUES (
|
|
140
|
-
if (target.auth === "internal")
|
|
141
|
-
this.ctx.storage.sql.exec("UPDATE _flared_outbox SET internal=1,auth='null' WHERE id=?", id);
|
|
139
|
+
this.ctx.storage.sql.exec("INSERT INTO _flared_outbox (id,release,path,args,auth,dueAt,internal) VALUES (?,?,?,?,?,?,?)", id, this.env.RELEASE_ID, path, JSON.stringify(input), JSON.stringify(target.auth === "internal" ? null : call.auth), Date.now() + Math.ceil(delay * 1000), Number(target.auth === "internal"));
|
|
142
140
|
return id;
|
|
143
141
|
})();
|
|
144
142
|
scheduled.push(pending);
|
|
@@ -161,16 +159,17 @@ export function createFacet(app) {
|
|
|
161
159
|
capability.revoke();
|
|
162
160
|
await Promise.allSettled(scheduled);
|
|
163
161
|
}
|
|
164
|
-
this.commitRevision(changed);
|
|
162
|
+
return this.commitRevision(changed);
|
|
165
163
|
});
|
|
166
164
|
await this.ctx.storage.sync();
|
|
167
|
-
return { result, revision
|
|
165
|
+
return { result, revision, tables: [...changed] };
|
|
168
166
|
});
|
|
169
167
|
}
|
|
170
168
|
commitRevision(changed) {
|
|
171
|
-
this.ctx.storage.sql.exec("UPDATE _flared_revision SET value=value+1 WHERE id=1");
|
|
169
|
+
const revision = Number(this.ctx.storage.sql.exec("UPDATE _flared_revision SET value=value+1 WHERE id=1 RETURNING value").one().value);
|
|
172
170
|
for (const name of changed)
|
|
173
|
-
this.ctx.storage.sql.exec("INSERT INTO _flared_changes VALUES (?,?) ON CONFLICT(name) DO UPDATE SET revision=excluded.revision", name,
|
|
171
|
+
this.ctx.storage.sql.exec("INSERT INTO _flared_changes VALUES (?,?) ON CONFLICT(name) DO UPDATE SET revision=excluded.revision", name, revision);
|
|
172
|
+
return revision;
|
|
174
173
|
}
|
|
175
174
|
pendingJobs(limit = 100) {
|
|
176
175
|
return this.serial(async () => {
|
|
@@ -295,7 +294,7 @@ export function createFacet(app) {
|
|
|
295
294
|
boundedJson(input, LIMITS.inputBytes);
|
|
296
295
|
let rows = [];
|
|
297
296
|
const changed = new Set;
|
|
298
|
-
await this.ctx.storage.transaction(async () => {
|
|
297
|
+
const revision = await this.ctx.storage.transaction(async () => {
|
|
299
298
|
if (input.expectedRevision !== this.revision())
|
|
300
299
|
throw new FlareError("CONFLICT", "Data changed; reload before editing");
|
|
301
300
|
const capability = database(this.ctx.storage.sql, app.schema, true, new Set, changed);
|
|
@@ -317,10 +316,10 @@ export function createFacet(app) {
|
|
|
317
316
|
} finally {
|
|
318
317
|
capability.revoke();
|
|
319
318
|
}
|
|
320
|
-
this.commitRevision(changed);
|
|
319
|
+
return this.commitRevision(changed);
|
|
321
320
|
});
|
|
322
321
|
await this.ctx.storage.sync();
|
|
323
|
-
return { rows, revision
|
|
322
|
+
return { rows, revision };
|
|
324
323
|
});
|
|
325
324
|
}
|
|
326
325
|
};
|