@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.
Files changed (34) hide show
  1. package/README.md +3 -1
  2. package/dist/cli/{chunk-5s6zv1h9.js → chunk-jrptpjmm.js} +1 -1
  3. package/dist/cli/{chunk-y6e2h40n.js → chunk-m0698kap.js} +37 -12
  4. package/dist/cli/{chunk-5yg1zj5n.js → chunk-znktbexz.js} +6 -6
  5. package/dist/cli/{chunk-vx7kmczm.js → chunk-zt96ghm7.js} +12 -6
  6. package/dist/cli/flare.js +5 -5
  7. package/dist/platform/admin.js +31 -23
  8. package/dist/platform/dashboard/_astro/{auth-view.EASEF9BN.js → auth-view.BhUFPmNv.js} +1 -1
  9. package/dist/platform/dashboard/_astro/{cli-approval.DwZVHdyo.js → cli-approval.emPTVRqn.js} +1 -1
  10. package/dist/platform/dashboard/_astro/client.4z54xJ_z.js +1 -0
  11. package/dist/platform/dashboard/_astro/{common.TAClb6G9.js → common.9pujiaSZ.js} +1 -1
  12. package/dist/platform/dashboard/_astro/{data-view.BaHEU4Xa.js → data-view.C-VU8Oxm.js} +1 -1
  13. package/dist/platform/dashboard/_astro/{functions-view.BGOYuDwu.js → functions-view.3yLrtS0h.js} +1 -1
  14. 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
  15. package/dist/platform/dashboard/_astro/{input-group.DUp-h0xx.js → input-group.BZ1TZT7L.js} +1 -1
  16. package/dist/platform/dashboard/_astro/{sdk-view.2ErPn4cO.js → sdk-view.BAiQKNm8.js} +1 -1
  17. package/dist/platform/dashboard/_astro/{services-view.iZujitxl.js → services-view.BSrqpyr5.js} +1 -1
  18. package/dist/platform/dashboard/_astro/{settings-view.CqK-mrlB.js → settings-view.C_eYjlIE.js} +1 -1
  19. package/dist/platform/dashboard/bootstrap/facet.js +64 -46
  20. package/dist/platform/dashboard/bootstrap/openapi.json +1 -1
  21. package/dist/platform/dashboard/bootstrap/release.json +2 -2
  22. package/dist/platform/dashboard/index.html +1 -1
  23. package/dist/platform/manifest.json +36 -36
  24. package/dist/platform/runtime.js +99 -66
  25. package/dist/sdk/backup.js +4 -4
  26. package/dist/sdk/client.js +22 -41
  27. package/dist/sdk/cloudflare.js +10 -11
  28. package/dist/sdk/database.js +45 -27
  29. package/dist/sdk/http.d.ts +2 -0
  30. package/dist/sdk/http.js +27 -0
  31. package/dist/sdk/schema.d.ts +2 -2
  32. package/dist/sdk/schema.js +7 -6
  33. package/package.json +2 -2
  34. package/dist/platform/dashboard/_astro/client.BUUZgEFj.js +0 -1
@@ -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 reader = request.body.getReader();
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((r) => this.user(String(r.id))),
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.8",
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), copied = [];
959
- for (const file of files) {
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
- copied.push({ ...file, md5: stored.etag });
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
- backups.push(meta ? {
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
- for (const file of value.files) {
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 at<? OR (at=? AND id<?) ORDER BY at DESC,id DESC LIMIT 21", before.at, before.at, before.id).toArray() : this.storage.sql.exec("SELECT * FROM backup_index ORDER BY at DESC,id DESC LIMIT 21").toArray();
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
- for (let offset = 0;offset < batch.messages.length; offset += 4) {
1189
- await Promise.all(batch.messages.slice(offset, offset + 4).map(async (message) => {
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), key = stableJson({ path: row.path, args: JSON.parse(row.args), auth });
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: JSON.parse(row.args),
8471
+ args,
8440
8472
  auth
8441
8473
  }));
8442
- this.ctx.storage.sql.exec("UPDATE subscriptions SET dependencies=?,checked=?,release=? WHERE connection=? AND id=?", JSON.stringify(result.tables), result.revision, this.current(), c.id, row.id);
8443
- if (row.hash === hash) {
8444
- this.ctx.storage.sql.exec("UPDATE subscriptions SET pending=0 WHERE connection=? AND id=?", c.id, row.id);
8445
- return;
8446
- }
8447
- c.send(boundedJson({
8448
- type: "snapshot",
8449
- id: row.id,
8450
- result: result.result,
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
- this.closeRevokedSockets();
8686
- await this.dashboardHint();
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
- this.closeRevokedSockets();
8983
+ if (op !== "auth-users" && op !== "auth-settings")
8984
+ this.closeRevokedSockets();
8952
8985
  return result;
8953
8986
  }
8954
8987
  if (op === "release-export") {
@@ -1,6 +1,6 @@
1
1
  import { boundedJson, FlareError, stableJson } from "./protocol.js";
2
2
  import {
3
- documentValue,
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 documentValue(schema[name], doc.value);
70
- if (stableJson(value) !== stableJson(doc.value))
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, stableJson(value));
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);
@@ -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: options.signal ?? AbortSignal.timeout(options.timeoutMs ?? 120000)
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.json().catch(() => null);
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(stableJson(value)) : value);
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 reader = response.body.getReader();
114
- let bytes = 0;
115
- const chunks = [];
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.hasValue = false;
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.hasValue = false;
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 = !sub.hasValue || stableJson(sub.value) !== json;
214
+ const changed = sub.snapshot !== json;
229
215
  sub.revision = frame.revision;
230
- sub.value = frame.result;
231
- sub.hasValue = true;
216
+ sub.snapshot = json;
232
217
  if (changed)
233
- notify(sub, "next", sub.value);
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.hasValue = false;
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.hasValue = false;
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.hasValue)
332
- notify({ ...sub, observers: new Set([observer]) }, "next", sub.value);
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.hasValue = false;
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"));
@@ -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 (?,?,?,?,?,?)", id, this.env.RELEASE_ID, path, JSON.stringify(input), JSON.stringify(call.auth), Date.now() + Math.ceil(delay * 1000));
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: this.revision(), tables: [...changed] };
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, this.revision());
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: this.revision() };
322
+ return { rows, revision };
324
323
  });
325
324
  }
326
325
  };