@vercel/board 0.26.0 → 0.26.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.
Files changed (35) hide show
  1. package/dist/{a2-Qzj0kCWU.mjs → a2-D6TOZNV2.mjs} +107 -39
  2. package/dist/{attempts-CLG840Oj.mjs → attempts-HXcskN1w.mjs} +1 -11
  3. package/dist/backend-a2/contract.mjs +1 -1
  4. package/dist/backend-a2.d.mts +3 -0
  5. package/dist/backend-a2.mjs +1 -1
  6. package/dist/{backend-http-F0W6e36G.mjs → backend-http-CDu_eapz.mjs} +1 -1
  7. package/dist/{backend-http-BB0Gld6Q.mjs → backend-http-DApbemsm.mjs} +18 -4
  8. package/dist/backend-http.mjs +1 -1
  9. package/dist/backend-http.node.mjs +1 -1
  10. package/dist/{backend-vercel-CtB46vMQ.mjs → backend-vercel-C2YcUuyT.mjs} +3 -3
  11. package/dist/{backend-vercel-DUWIcFi9.mjs → backend-vercel-QuUJ3dW_.mjs} +8 -3
  12. package/dist/backend-vercel.mjs +1 -1
  13. package/dist/backend-vercel.node.mjs +1 -1
  14. package/dist/cli-embedded.mjs +1 -1
  15. package/dist/cli.mjs +2 -2
  16. package/dist/{engine-ROa7-IQY.mjs → engine-Bfe6pcXb.mjs} +2 -2
  17. package/dist/{exports-NFjBgIT2.mjs → exports-T-ART7P7.mjs} +2 -2
  18. package/dist/{index.default-Boe7x-mK.mjs → index.default-_e7u6Z-I.mjs} +1 -1
  19. package/dist/index.mjs +3 -3
  20. package/dist/index.node.mjs +1 -1
  21. package/dist/inspect/app.js +29 -29
  22. package/dist/inspect/engine.js +5 -5
  23. package/dist/{main-BuahKR16.mjs → main-Ci7R6qkK.mjs} +9 -4
  24. package/dist/{reducers-BFmIXilu.mjs → reducers-BcmSs4qf.mjs} +3 -1
  25. package/dist/{serve-BbHEQk_Y.mjs → serve-CNklIudj.mjs} +2 -2
  26. package/dist/{server-Bphio_82.mjs → server-DMNfPSAZ.mjs} +3 -3
  27. package/dist/sync-linear.mjs +6 -5
  28. package/dist/{ids-C6AYo1bu.mjs → wait-until-Dw4XrIDQ.mjs} +12 -1
  29. package/dist/{watchers-hash-BfDPW95j.mjs → watchers-hash-C10qKYPa.mjs} +1 -1
  30. package/docs/01-board/06-watchers.md +4 -2
  31. package/docs/01-board/07-schedules.md +12 -5
  32. package/docs/02-sync/07-linear.md +4 -2
  33. package/docs/04-reference/01-cli.md +2 -1
  34. package/docs/04-reference/03-http.md +1 -1
  35. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
1
  import { n as DIRECT_UPLOAD_BYTES_MAX, r as IMPORT_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
2
2
  import { _ as RemoteWatcherId, b as actorKind, i as Attachment, x as platformAvatar } from "./schemas-Be08nyfW.mjs";
3
3
  import "./external-BV_cUncs.mjs";
4
- import { a as METHODS, d as VERSION, f as SYNC, n as compileWatcher, r as hasCriteria, t as canonicalWatchers } from "./watchers-hash-BfDPW95j.mjs";
4
+ import { a as METHODS, d as VERSION, f as SYNC, n as compileWatcher, r as hasCriteria, t as canonicalWatchers } from "./watchers-hash-C10qKYPa.mjs";
5
5
  import { r as duration, t as BoardError } from "./client-D0Px7KzF.mjs";
6
- import { n as derivedId, r as isKeyIdOf } from "./ids-C6AYo1bu.mjs";
6
+ import { i as isKeyIdOf, r as derivedId, t as waitUntil } from "./wait-until-Dw4XrIDQ.mjs";
7
7
  import { a as forbidden, c as toErrorResponse, i as duplicateId, r as badRequest, s as notFound, t as HttpError } from "./errors-CusN4x2l.mjs";
8
8
  import { t as own } from "./own-OwgPWdmh.mjs";
9
9
  import { n as replaceMentions } from "./mentions-CB6ShY4R.mjs";
@@ -13,12 +13,12 @@ import { n as MIDDLEWARE_ACTOR, t as DroppedError } from "./middleware-ChKM1zuu.
13
13
  import { n as syncTarget, r as withEmailMentions } from "./targets-C-wdR5wP.mjs";
14
14
  import { t as A2Error } from "./session-clear-wHd5GCpZ-BnKSxReK.mjs";
15
15
  import { a as reducer } from "./reducer-CjmvZSAb-D_OSISmj.mjs";
16
- import { _ as BOARD_SESSION, a as IDENTITY_SESSION, c as SCHEDULES_SESSION, d as inboxContract, f as inboxSession, g as watchSession, h as watchContract, l as boardContract, m as schedulesContract, n as channelsReducer, o as INBOXES_SESSION, p as registryContract, r as identityCodesReducer, s as REGISTRY_SESSION, t as actorsReducer, u as identityContract } from "./reducers-BFmIXilu.mjs";
16
+ import { _ as BOARD_SESSION, a as IDENTITY_SESSION, c as SCHEDULES_SESSION, d as inboxContract, f as inboxSession, g as watchSession, h as watchContract, l as boardContract, m as schedulesContract, n as channelsReducer, o as INBOXES_SESSION, p as registryContract, r as identityCodesReducer, s as REGISTRY_SESSION, t as actorsReducer, u as identityContract } from "./reducers-BcmSs4qf.mjs";
17
17
  import { n as createServer, t as consumer } from "./server-DtFrWxi3-Boa6gdWO.mjs";
18
18
  import { a as linkedTo, i as iso, n as foldMessages, o as personOf, t as foldActors } from "./folds-Dyjput8M.mjs";
19
19
  import { t as concernedMessageId } from "./project-DMDST9kl.mjs";
20
20
  import { t as TABLES_CONSUMER } from "./sql-U9WTxrwJ.mjs";
21
- import { n as waitUntil, t as afterRetry } from "./attempts-CLG840Oj.mjs";
21
+ import { t as afterRetry } from "./attempts-HXcskN1w.mjs";
22
22
  import { createHash, randomBytes } from "node:crypto";
23
23
  import { mkdir, readFile, writeFile } from "node:fs/promises";
24
24
  import { dirname, join } from "node:path";
@@ -1277,13 +1277,22 @@ function foldSchedules(state, event) {
1277
1277
  schedule.next = event.payload.next;
1278
1278
  return state;
1279
1279
  }
1280
+ case "schedule.takeover":
1281
+ state.takeover = {
1282
+ deployment: event.payload.deployment,
1283
+ index: event.index
1284
+ };
1285
+ return state;
1280
1286
  default: return state;
1281
1287
  }
1282
1288
  }
1283
1289
  const schedulesReducer = reducer({
1284
1290
  contract: schedulesContract,
1285
- name: "board.schedules.v1",
1286
- initialState: { byId: {} }
1291
+ name: "board.schedules.v2",
1292
+ initialState: {
1293
+ byId: {},
1294
+ takeover: null
1295
+ }
1287
1296
  }).fold(foldSchedules);
1288
1297
  /** The send after one due at `at`: the next slot after now, so downtime skips missed slots instead of replaying them. */
1289
1298
  function nextSend(schedule, n, at, now) {
@@ -1345,10 +1354,23 @@ function createSchedules(options) {
1345
1354
  let accessNow = null;
1346
1355
  /** Posts as `identity`, through what every post does: the middleware too. */
1347
1356
  let send = null;
1348
- /** Arms send `n`, due at `at`: the whole way, or one hop toward it. */
1349
- async function arm(ctx, name, id, n, at) {
1350
- const hop = Math.min(at, Date.now() + MAX_HOP_MS);
1351
- await ctx.session.schedule(name, { at: new Date(Math.max(hop, Date.now())) }, {
1357
+ /** Vercel Queues delivers a timer only to the deployment that armed it, which runs its own code and access rules. */
1358
+ const deployment = process.env.VERCEL_DEPLOYMENT_ID || null;
1359
+ /**
1360
+ * Where this deployment's timers land: a session of its own on Vercel. A queue drains one session
1361
+ * at a time, and Vercel Queues drains it on the deployment that appended to it, so an earlier
1362
+ * deployment never drains the one that sends' timers, and can't drop them there.
1363
+ */
1364
+ const sends = deployment === null ? SCHEDULES_SESSION : `${SCHEDULES_SESSION}:${deployment}`;
1365
+ /**
1366
+ * Arms send `n`, due at `at`: the whole way, or one hop toward it. A timer's name is its identity
1367
+ * in the queue, so arming the same send under the same name again adds nothing; only a hop's names
1368
+ * the hop, since a send waits through several.
1369
+ */
1370
+ async function arm(name, id, n, at) {
1371
+ const hop = Math.max(Math.min(at, Date.now() + MAX_HOP_MS), Date.now());
1372
+ const named = name === "hop" ? `hop-${id}-${n}-${hop}` : `${name}-${id}-${n}`;
1373
+ await session.forSends.schedule(named, { at: new Date(hop) }, {
1352
1374
  type: "schedule.due",
1353
1375
  payload: {
1354
1376
  id,
@@ -1357,24 +1379,33 @@ function createSchedules(options) {
1357
1379
  }
1358
1380
  });
1359
1381
  }
1382
+ /** Appends to the schedules, once for each `key`, from a send in any session. */
1383
+ async function record(event, ...key) {
1384
+ await session.append({
1385
+ ...event,
1386
+ id: await idempotentId("board-schedule-record", ...key)
1387
+ });
1388
+ }
1360
1389
  async function due(ctx) {
1361
1390
  if (!writes || !accessNow || !send) throw new Error("schedules used before the board bound them");
1362
1391
  const { id, n, at } = ctx.event.payload;
1363
- const { state } = await ctx.session.state(schedulesReducer);
1392
+ const { state } = await session.state(schedulesReducer, { through: "latest" });
1364
1393
  const schedule = state.byId[id];
1394
+ if (deployment !== null && state.takeover !== null && state.takeover.deployment !== deployment) return;
1395
+ if (schedule && ctx.attempt > 1 && schedule.sent === n + 1 && schedule.next !== null) return arm("next", id, n + 1, schedule.next);
1365
1396
  if (!schedule || schedule.next !== at || schedule.sent !== n) return;
1366
- if (Date.now() < at - EARLY_MS) return arm(ctx, "hop", id, n, at);
1397
+ if (Date.now() < at - EARLY_MS) return arm("hop", id, n, at);
1367
1398
  if (schedule.re !== null) {
1368
1399
  const root = await writes.reads.message(schedule.re, ctx.signal);
1369
1400
  if (!root || root.deleted) {
1370
- await ctx.session.append("ended", {
1401
+ await record({
1371
1402
  type: "schedule.canceled",
1372
1403
  payload: {
1373
1404
  id,
1374
1405
  by: SYSTEM.author,
1375
1406
  reason: "thread_deleted"
1376
1407
  }
1377
- });
1408
+ }, "ended", String(id));
1378
1409
  return;
1379
1410
  }
1380
1411
  }
@@ -1386,14 +1417,14 @@ function createSchedules(options) {
1386
1417
  }).catch(refused);
1387
1418
  if (!access || !proofs) {
1388
1419
  console.info(`[board] schedule s${id}'s creator can no longer post it in ${schedule.channel}`);
1389
- await ctx.session.append("ended", {
1420
+ await record({
1390
1421
  type: "schedule.canceled",
1391
1422
  payload: {
1392
1423
  id,
1393
1424
  by: SYSTEM.author,
1394
1425
  reason: "revoked"
1395
1426
  }
1396
- });
1427
+ }, "ended", String(id));
1397
1428
  return;
1398
1429
  }
1399
1430
  const input = {
@@ -1426,18 +1457,18 @@ function createSchedules(options) {
1426
1457
  } catch (error) {
1427
1458
  if (!(error instanceof DroppedError)) throw error;
1428
1459
  console.info(`[board] the middleware dropped schedule s${id}: ${error.message}`);
1429
- await ctx.session.append("ended", {
1460
+ await record({
1430
1461
  type: "schedule.canceled",
1431
1462
  payload: {
1432
1463
  id,
1433
1464
  by: SYSTEM.author,
1434
1465
  reason: "dropped"
1435
1466
  }
1436
- });
1467
+ }, "ended", String(id));
1437
1468
  return;
1438
1469
  }
1439
1470
  const next = nextSend(schedule, n, at, Date.now());
1440
- await ctx.session.append("fired", {
1471
+ await record({
1441
1472
  type: "schedule.fired",
1442
1473
  payload: {
1443
1474
  id,
@@ -1445,13 +1476,12 @@ function createSchedules(options) {
1445
1476
  message: posted,
1446
1477
  next
1447
1478
  }
1448
- });
1449
- if (next !== null) await arm(ctx, "next", id, n + 1, next);
1479
+ }, "fired", String(id), String(n));
1480
+ if (next !== null) await arm("next", id, n + 1, next);
1450
1481
  }
1451
1482
  /**
1452
- * Arms each schedule's next send, as the schedule stands now, from the first schedule on: a queue
1453
- * delivers a timer to the deployment that armed it, so one an earlier deployment armed may never
1454
- * reach this one. The deployment that gets there first sends, and `due` makes the other's a no-op.
1483
+ * Arms each schedule's next send, as the schedule stands now, from the first schedule on: the
1484
+ * first deploy with this consumer arms every schedule from before it.
1455
1485
  */
1456
1486
  const arming = consumer({
1457
1487
  contract: schedulesContract,
@@ -1464,7 +1494,7 @@ function createSchedules(options) {
1464
1494
  const { state } = await ctx.session.state(schedulesReducer, { through: "latest" });
1465
1495
  const schedule = state.byId[ctx.event.index];
1466
1496
  if (!schedule || schedule.next === null) return;
1467
- await arm(ctx, "first", schedule.id, schedule.sent, schedule.next);
1497
+ await arm("first", schedule.id, schedule.sent, schedule.next);
1468
1498
  });
1469
1499
  /** Makes each due send, one schedule at a time. */
1470
1500
  const sending = consumer({
@@ -1479,19 +1509,53 @@ function createSchedules(options) {
1479
1509
  ...options.scheduler ? { scheduler: options.scheduler } : {},
1480
1510
  consumers: [arming, sending]
1481
1511
  });
1482
- const session = server.session(SCHEDULES_SESSION);
1512
+ const session = Object.assign(server.session(SCHEDULES_SESSION), { forSends: server.session(sends) });
1513
+ const scheduled = options.scheduler !== void 0;
1514
+ /**
1515
+ * Makes this deployment the one that sends, unless it already is, and arms every pending
1516
+ * schedule's next send here, since the timers an earlier deployment armed only reach it. Each
1517
+ * instance arms them again, under the same names, so one created on an earlier deployment
1518
+ * after the takeover, or missed by a takeover that failed partway, still gets a timer here.
1519
+ */
1520
+ async function takeOver() {
1521
+ if (deployment === null) return;
1522
+ const read = await session.state(schedulesReducer);
1523
+ let { state } = read;
1524
+ if (state.takeover?.deployment !== deployment) {
1525
+ await session.append({
1526
+ type: "schedule.takeover",
1527
+ payload: { deployment },
1528
+ id: await idempotentId("board-schedule-takeover", deployment, String(read.index))
1529
+ });
1530
+ state = (await session.state(schedulesReducer)).state;
1531
+ }
1532
+ const since = state.takeover.index;
1533
+ for (const schedule of Object.values(state.byId)) if (schedule.next !== null) await arm(`takeover-${since}`, schedule.id, schedule.sent, schedule.next);
1534
+ }
1483
1535
  /**
1484
- * The first request on an instance starts the arming catching up, and doesn't wait: on the
1485
- * first deploy with it, every schedule from before gets its next send armed here.
1536
+ * The first call on an instance starts the arming catching up, and doesn't wait: on the first
1537
+ * deploy with it, every schedule from before gets its next send armed here. The first call that
1538
+ * isn't a queue's delivery takes schedules over too: a deployment nobody visits any more still
1539
+ * gets its own timers, and those mustn't take schedules back from the one people reach.
1486
1540
  */
1487
1541
  let catching = null;
1488
- function catchUp() {
1489
- if (catching || !options.scheduler) return;
1490
- catching = session.consume(arming).catch((error) => {
1491
- console.warn("[board] schedules are not caught up:", error.message);
1492
- catching = null;
1493
- });
1494
- waitUntil(catching);
1542
+ let taking = null;
1543
+ function catchUp(options) {
1544
+ if (!scheduled) return;
1545
+ if (!catching) {
1546
+ catching = session.consume(arming).catch((error) => {
1547
+ console.warn("[board] schedules are not caught up:", error.message);
1548
+ catching = null;
1549
+ });
1550
+ waitUntil(catching);
1551
+ }
1552
+ if (options.takeOver && !taking) {
1553
+ taking = takeOver().catch((error) => {
1554
+ console.warn("[board] schedules are not taken over:", error.message);
1555
+ taking = null;
1556
+ });
1557
+ waitUntil(taking);
1558
+ }
1495
1559
  }
1496
1560
  /** `visible`: which schedules the caller may see. */
1497
1561
  async function list(query, visible = () => true) {
@@ -1562,8 +1626,10 @@ function createSchedules(options) {
1562
1626
  }
1563
1627
  });
1564
1628
  const { state } = await session.state(schedulesReducer);
1629
+ const created = state.byId[row.index];
1630
+ if (created.next !== null) await arm("first", created.id, created.sent, created.next);
1565
1631
  const { state: actors } = await writes.session.state(actorsReducer);
1566
- const schedule = scheduleView(state.byId[row.index], actors);
1632
+ const schedule = scheduleView(created, actors);
1567
1633
  return unknown.length && identity.principal.access.readable(channel) ? {
1568
1634
  schedule,
1569
1635
  unknownMentions: unknown
@@ -2467,7 +2533,7 @@ function scheduleAt(value) {
2467
2533
  return index;
2468
2534
  }
2469
2535
  new AbortController().signal;
2470
- /** A wake-up only has to arrive: the board it wakes runs what it pulls before it answers. */
2536
+ /** A wake-up only has to arrive: the board it wakes answers once what it pulls has run, or after 2 seconds while it keeps going. */
2471
2537
  const WAKE_TIMEOUT_MS = 5e3;
2472
2538
  /**
2473
2539
  * `idempotentId`'s shape, a UUIDv8: the IDs the board derives for its own writes (schedule
@@ -3969,6 +4035,8 @@ function a2(options = {}) {
3969
4035
  if (!binding) throw new Error("a2: this backend isn't serving a board yet: pass it to board({ backend })");
3970
4036
  throw new Error("a2: its default store opens on the board's first request: pass a store to reach it before that");
3971
4037
  };
4038
+ /** A queue's delivery, through the scheduler callback, and everything it runs: watchers' writes too. */
4039
+ const delivery = new AsyncLocalStorage();
3972
4040
  const ready = async () => {
3973
4041
  if (!built) {
3974
4042
  if (!binding) throw new Error("a2: this backend isn't serving a board yet: pass it to board({ backend })");
@@ -3980,7 +4048,7 @@ function a2(options = {}) {
3980
4048
  opening.catch(() => opening = null);
3981
4049
  await opening;
3982
4050
  }
3983
- built.schedules.catchUp();
4051
+ built.schedules.catchUp({ takeOver: !delivery.getStore() });
3984
4052
  return built;
3985
4053
  };
3986
4054
  const backend = {
@@ -3996,7 +4064,7 @@ function a2(options = {}) {
3996
4064
  }, given);
3997
4065
  },
3998
4066
  /** The scheduler's callback: `export { scheduler as POST }` from a route of its own. A board without a scheduler answers 404. */
3999
- scheduler: async (request) => (await ready()).scheduler(request),
4067
+ scheduler: (request) => delivery.run(true, async () => (await ready()).scheduler(request)),
4000
4068
  /** The A2 servers behind this board, to share one scheduler with other A2 servers. */
4001
4069
  get servers() {
4002
4070
  return now().servers;
@@ -1,13 +1,3 @@
1
- //#region src/server/wait-until.ts
2
- /**
3
- * Keeps `task` running after the response, where the platform offers it: Vercel's request context, as A2
4
- * reads it too. Elsewhere it runs on in the process.
5
- */
6
- function waitUntil(task) {
7
- try {
8
- Reflect.get(globalThis, Symbol.for("@vercel/request-context"))?.get?.()?.waitUntil?.(task);
9
- } catch {}
10
- }
11
1
  /**
12
2
  * Whether a run comes after a retry from a dead letter. A2 keeps counting
13
3
  * `ctx.attempt` across one, and no earlier run sees an attempt past A2_TRIES:
@@ -17,4 +7,4 @@ function waitUntil(task) {
17
7
  */
18
8
  const afterRetry = (attempt) => attempt > 10;
19
9
  //#endregion
20
- export { waitUntil as n, afterRetry as t };
10
+ export { afterRetry as t };
@@ -1,5 +1,5 @@
1
1
  import { n as DIRECT_UPLOAD_BYTES_MAX, r as IMPORT_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "../files-SniSmeEr.mjs";
2
2
  import { a as AttachmentId, c as ChannelPath, d as LabelKey, f as LabelValue, g as MessageText, h as MessageId, i as Attachment, l as Emoji, m as MESSAGE_TEXT_MAX, n as ActorId, o as Author, p as Labels, r as ActorKind, s as CHANNEL_PATH_MAX, t as ATTACHMENTS_PER_MESSAGE_MAX, u as Handle } from "../schemas-Be08nyfW.mjs";
3
3
  import { o as Origin } from "../external-BV_cUncs.mjs";
4
- import { _ as BOARD_SESSION, i as messagesReducer, l as boardContract, n as channelsReducer, t as actorsReducer } from "../reducers-BFmIXilu.mjs";
4
+ import { _ as BOARD_SESSION, i as messagesReducer, l as boardContract, n as channelsReducer, t as actorsReducer } from "../reducers-BcmSs4qf.mjs";
5
5
  export { ATTACHMENTS_PER_MESSAGE_MAX, ATTACHMENT_BYTES_MAX, ActorId, ActorKind, Attachment, AttachmentId, Author, BOARD_SESSION, CHANNEL_PATH_MAX, ChannelPath, DIRECT_UPLOAD_BYTES_MAX, Emoji, Handle, IMPORT_BYTES_MAX, LabelKey, LabelValue, Labels, MESSAGE_TEXT_MAX, MessageId, MessageText, Origin, actorsReducer, boardContract, channelsReducer, messagesReducer };
@@ -1194,6 +1194,9 @@ export declare function a2(options?: A2Options): {
1194
1194
  message: z.ZodNumber;
1195
1195
  next: z.ZodNullable<z.ZodNumber>;
1196
1196
  }, z.core.$strip>;
1197
+ 'schedule.takeover': z.ZodObject<{
1198
+ deployment: z.ZodString;
1199
+ }, z.core.$strip>;
1197
1200
  }, Record<never, never>, undefined>)[];
1198
1201
  session: {
1199
1202
  readonly id: string;
@@ -1,2 +1,2 @@
1
- import { n as fsBlob, r as memoryBlob, t as a2 } from "./a2-Qzj0kCWU.mjs";
1
+ import { n as fsBlob, r as memoryBlob, t as a2 } from "./a2-D6TOZNV2.mjs";
2
2
  export { a2, fsBlob, memoryBlob };
@@ -1,4 +1,4 @@
1
- import { r as createHttp } from "./backend-http-BB0Gld6Q.mjs";
1
+ import { r as createHttp } from "./backend-http-DApbemsm.mjs";
2
2
  import { n as connect, r as environmentProfile, t as boardUrl } from "./node-Ybi9E6bO.mjs";
3
3
  //#region src/backend-http/index.ts
4
4
  const environment = {
@@ -1,14 +1,16 @@
1
1
  import "./files-SniSmeEr.mjs";
2
2
  import "./schemas-Be08nyfW.mjs";
3
- import { K as routes, O as RegisterBody, a as METHODS, n as compileWatcher, t as canonicalWatchers } from "./watchers-hash-BfDPW95j.mjs";
3
+ import { K as routes, O as RegisterBody, a as METHODS, n as compileWatcher, t as canonicalWatchers } from "./watchers-hash-C10qKYPa.mjs";
4
4
  import { n as createBoardClient, t as BoardError } from "./client-D0Px7KzF.mjs";
5
- import { a as sha256, i as keyId, n as derivedId, t as DERIVED_ID } from "./ids-C6AYo1bu.mjs";
5
+ import { a as keyId, n as DERIVED_ID, o as sha256, r as derivedId, t as waitUntil } from "./wait-until-Dw4XrIDQ.mjs";
6
6
  import { a as forbidden, o as json, t as HttpError } from "./errors-CusN4x2l.mjs";
7
7
  //#region src/server/registrant.ts
8
8
  /** Registering again this soon after the same registration only repeats it. */
9
9
  const REGISTER_EVERY_MS = 1e4;
10
10
  /** How long one wake-up runs matches before it leaves the rest to the next. */
11
11
  const DRAIN_MS = 24e4;
12
+ /** How long a wake-up waits for what it pulled to run before it answers: under the 3 seconds Vercel's API gives it. */
13
+ const WAKE_ANSWER_MS = 2e3;
12
14
  /** A pull with nothing to hand out holds this long, when the board pulls on its own. */
13
15
  const POLL_WAIT_S = 25;
14
16
  const describe$1 = (error) => ((error instanceof Error ? error.message : String(error)) || "unknown error").slice(0, 500);
@@ -142,8 +144,20 @@ function registrant(options) {
142
144
  path: "/wake",
143
145
  async handle(request) {
144
146
  started ??= Promise.resolve();
145
- if (request.headers.get("x-board-watchers") !== await options.hash()) await register(wakeable(bound().url())).catch(rethrow);
146
- return json({ ran: await drain().catch(rethrow) });
147
+ const stale = request.headers.get("x-board-watchers") !== await options.hash();
148
+ const running = (async () => {
149
+ if (stale) await register(wakeable(bound().url()));
150
+ return drain();
151
+ })();
152
+ waitUntil(running.catch((error) => console.error(`[board] couldn't run what woke it:`, describe$1(error))));
153
+ let timer;
154
+ const waited = new Promise((resolve) => timer = setTimeout(() => resolve(null), WAKE_ANSWER_MS));
155
+ try {
156
+ const ran = await Promise.race([running.catch(rethrow), waited]);
157
+ return ran === null ? new Response(null, { status: 202 }) : json({ ran });
158
+ } finally {
159
+ clearTimeout(timer);
160
+ }
147
161
  }
148
162
  }, {
149
163
  method: "POST",
@@ -1,2 +1,2 @@
1
- import { t as http } from "./index.default-Boe7x-mK.mjs";
1
+ import { t as http } from "./index.default-_e7u6Z-I.mjs";
2
2
  export { http };
@@ -1,2 +1,2 @@
1
- import { t as http } from "./backend-http-F0W6e36G.mjs";
1
+ import { t as http } from "./backend-http-CDu_eapz.mjs";
2
2
  export { http };
@@ -1,8 +1,8 @@
1
1
  import { _ as RemoteWatcherId } from "./schemas-Be08nyfW.mjs";
2
- import { O as RegisterBody, a as METHODS, d as VERSION, n as compileWatcher, t as canonicalWatchers } from "./watchers-hash-BfDPW95j.mjs";
3
- import { a as sha256, n as derivedId } from "./ids-C6AYo1bu.mjs";
2
+ import { O as RegisterBody, a as METHODS, d as VERSION, n as compileWatcher, t as canonicalWatchers } from "./watchers-hash-C10qKYPa.mjs";
3
+ import { o as sha256, r as derivedId } from "./wait-until-Dw4XrIDQ.mjs";
4
4
  import { a as forbidden, o as json, s as notFound, t as HttpError } from "./errors-CusN4x2l.mjs";
5
- import { a as wakePath, i as registrant, n as callerProfile, t as PROJECTS_OWN } from "./backend-http-BB0Gld6Q.mjs";
5
+ import { a as wakePath, i as registrant, n as callerProfile, t as PROJECTS_OWN } from "./backend-http-DApbemsm.mjs";
6
6
  //#region src/server/backend-vercel.ts
7
7
  const PROFILE = /^[A-Za-z][A-Za-z0-9-]{0,31}$/;
8
8
  const SESSION = /^[A-Za-z0-9_.:-]{1,128}$/;
@@ -1,6 +1,6 @@
1
1
  import { t as HttpError } from "./errors-CusN4x2l.mjs";
2
2
  import { c as vercelLogin, l as vercelProjectFile, o as loginHanded, r as environmentProfile } from "./node-Ybi9E6bO.mjs";
3
- import { t as createVercel } from "./backend-vercel-CtB46vMQ.mjs";
3
+ import { t as createVercel } from "./backend-vercel-C2YcUuyT.mjs";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import { decodeJwt } from "jose";
6
6
  import { getVercelOidcToken } from "@vercel/oidc";
@@ -38,10 +38,15 @@ function handTeam(resolve) {
38
38
  }
39
39
  /**
40
40
  * The team: the one a CLI that runs board's hands it, `VERCEL_ORG_ID`, the one the deployment's OIDC
41
- * token names, or the linked project's (the nearest `.vercel/project.json`).
41
+ * token names, or the linked project's (the nearest `.vercel/project.json`). A handed one that
42
+ * throws fails the request with what it threw: the app's own words, not an internal error.
42
43
  */
43
44
  async function team() {
44
- if (handed) return handed();
45
+ if (handed) try {
46
+ return await handed();
47
+ } catch (error) {
48
+ throw new HttpError(502, "no_team", error instanceof Error ? error.message : String(error), { origin: "handle" });
49
+ }
45
50
  if (process.env.VERCEL_ORG_ID) return process.env.VERCEL_ORG_ID;
46
51
  if (!process.env.VERCEL_TOKEN && deployed()) try {
47
52
  const owner = decodeJwt(await getVercelOidcToken()).owner_id;
@@ -1,4 +1,4 @@
1
- import { t as createVercel } from "./backend-vercel-CtB46vMQ.mjs";
1
+ import { t as createVercel } from "./backend-vercel-C2YcUuyT.mjs";
2
2
  //#region src/backend-vercel/index.default.ts
3
3
  /**
4
4
  * Your team's board, in Vercel's API. Outside Node there's no environment to find credentials or
@@ -1,2 +1,2 @@
1
- import { n as handTeam, r as vercel, t as credentialSource } from "./backend-vercel-DUWIcFi9.mjs";
1
+ import { n as handTeam, r as vercel, t as credentialSource } from "./backend-vercel-QuUJ3dW_.mjs";
2
2
  export { credentialSource, handTeam, vercel };
@@ -1,4 +1,4 @@
1
- import { a as commandLine, c as failureLines, i as mainWith, l as formatFailure, n as commandWord, o as exitCode, r as main, s as failureDocument, t as Failed } from "./main-BuahKR16.mjs";
1
+ import { a as commandLine, c as failureLines, i as mainWith, l as formatFailure, n as commandWord, o as exitCode, r as main, s as failureDocument, t as Failed } from "./main-Ci7R6qkK.mjs";
2
2
  //#region src/cli/embedded.ts
3
3
  /**
4
4
  * Board's CLI as `appName` names it in help, usage, and errors (`vercel board`). With no board
package/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import { t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
3
3
  import "./schemas-Be08nyfW.mjs";
4
4
  import { a as option } from "./commands-nEG47RM_.mjs";
5
5
  import { s as projectDir } from "./node-Ybi9E6bO.mjs";
6
- import { r as main, u as who } from "./main-BuahKR16.mjs";
6
+ import { r as main, u as who } from "./main-Ci7R6qkK.mjs";
7
7
  import { randomBytes, timingSafeEqual } from "node:crypto";
8
8
  import { appendFile, readFile } from "node:fs/promises";
9
9
  import { join } from "node:path";
@@ -661,7 +661,7 @@ const local = {
661
661
  if (args.length > 0) throw usage("serve takes no arguments: pass its port with --port, and its directory with --dir.");
662
662
  const port = portFlag();
663
663
  const root = projectDir();
664
- const { localHandle, serveLocal } = await import("./serve-BbHEQk_Y.mjs");
664
+ const { localHandle, serveLocal } = await import("./serve-CNklIudj.mjs");
665
665
  const handle = flags.handle ?? process.env.BOARD_HANDLE;
666
666
  const local = await serveLocal({
667
667
  dir: flags.dir ?? join(root, ".board"),
@@ -2,11 +2,11 @@ import { a as typeOf, i as fileSize, r as IMPORT_BYTES_MAX, t as ATTACHMENT_BYTE
2
2
  import { C as toStorable, c as ChannelPath, m as MESSAGE_TEXT_MAX } from "./schemas-Be08nyfW.mjs";
3
3
  import { c as externalMessageKey, l as isThreadTarget, s as channelOf } from "./external-BV_cUncs.mjs";
4
4
  import { t as canonicalEmoji } from "./emoji-CTLpvH-A.mjs";
5
- import { n as derivedId } from "./ids-C6AYo1bu.mjs";
5
+ import { r as derivedId, t as waitUntil } from "./wait-until-Dw4XrIDQ.mjs";
6
6
  import { r as badRequest, s as notFound, t as HttpError } from "./errors-CusN4x2l.mjs";
7
7
  import { n as replaceMentions } from "./mentions-CB6ShY4R.mjs";
8
8
  import { t as DroppedError } from "./middleware-ChKM1zuu.mjs";
9
- import { n as waitUntil, t as afterRetry } from "./attempts-CLG840Oj.mjs";
9
+ import { t as afterRetry } from "./attempts-HXcskN1w.mjs";
10
10
  import { a as channelMissing, i as UnreadableError, n as ResolvedTarget, o as routesFor, t as ReceivedEvent } from "./provider-poIkXapd.mjs";
11
11
  import { a as immutable, n as SyncOverrides } from "./events-CewD4kw7.mjs";
12
12
  import { t as displayText } from "./bots-CGkO76qH.mjs";
@@ -1,8 +1,8 @@
1
1
  import { t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
2
2
  import "./schemas-Be08nyfW.mjs";
3
- import { A as SchedulesQuery, B as SyncReceiptBody, C as MatchesQuery, D as Ref, E as ReactBody, F as SyncBody, G as UploadStartBody, H as SyncUnlinkBody, I as SyncCodeBody, L as SyncCopiesQuery, M as SourceJoinBody, N as SourceReadQuery, O as RegisterBody, P as SyncAccountBody, R as SyncFindQuery, S as LinkedQuery, T as PostBody, U as ThreadsQuery, V as SyncRootsQuery, W as TypingBody, _ as InboxAckBody, a as METHODS, b as InboxQuery, c as queryObject, d as VERSION, f as SYNC, g as IdentityUnlinkBody, h as EventsQuery, j as SignInQuery, k as ScheduleBody, l as readCapped, m as EditBody, o as createRouter, p as AckBody, s as openapi, u as readJson, v as InboxNextQuery, w as MessagesQuery, x as LabelBody, y as InboxPushBody, z as SyncImportBody } from "./watchers-hash-BfDPW95j.mjs";
3
+ import { A as SchedulesQuery, B as SyncReceiptBody, C as MatchesQuery, D as Ref, E as ReactBody, F as SyncBody, G as UploadStartBody, H as SyncUnlinkBody, I as SyncCodeBody, L as SyncCopiesQuery, M as SourceJoinBody, N as SourceReadQuery, O as RegisterBody, P as SyncAccountBody, R as SyncFindQuery, S as LinkedQuery, T as PostBody, U as ThreadsQuery, V as SyncRootsQuery, W as TypingBody, _ as InboxAckBody, a as METHODS, b as InboxQuery, c as queryObject, d as VERSION, f as SYNC, g as IdentityUnlinkBody, h as EventsQuery, j as SignInQuery, k as ScheduleBody, l as readCapped, m as EditBody, o as createRouter, p as AckBody, s as openapi, u as readJson, v as InboxNextQuery, w as MessagesQuery, x as LabelBody, y as InboxPushBody, z as SyncImportBody } from "./watchers-hash-C10qKYPa.mjs";
4
4
  import { n as createBoardClient, t as BoardError } from "./client-D0Px7KzF.mjs";
5
- import { n as derivedId } from "./ids-C6AYo1bu.mjs";
5
+ import { r as derivedId } from "./wait-until-Dw4XrIDQ.mjs";
6
6
  import { a as forbidden, c as toErrorResponse, l as unauthorized, n as RejectedError, o as json, r as badRequest, s as notFound, t as HttpError } from "./errors-CusN4x2l.mjs";
7
7
  import { d as issuersOf, g as verifiesTokens, l as hostPrincipal, o as createAuthority, s as fromTokens } from "./auth-lTkOgvcE.mjs";
8
8
  import { i as resolveActor, r as claimsOf, t as KNOWN_AGENTS } from "./actors-BLXVp4oZ.mjs";
@@ -1,4 +1,4 @@
1
- import { r as createHttp } from "./backend-http-BB0Gld6Q.mjs";
1
+ import { r as createHttp } from "./backend-http-DApbemsm.mjs";
2
2
  //#region src/backend-http/index.default.ts
3
3
  /**
4
4
  * The board at `url`, which keeps the log. Outside Node there's no environment to find it or a
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { r as duration, t as BoardError } from "./client-D0Px7KzF.mjs";
2
2
  import { t as HttpError } from "./errors-CusN4x2l.mjs";
3
- import { t as http } from "./index.default-Boe7x-mK.mjs";
4
- import { t as createBoard } from "./exports-NFjBgIT2.mjs";
3
+ import { t as http } from "./index.default-_e7u6Z-I.mjs";
4
+ import { t as createBoard } from "./exports-T-ART7P7.mjs";
5
5
  import { t as DroppedError } from "./middleware-ChKM1zuu.mjs";
6
6
  //#region src/server/index.default.ts
7
7
  /**
@@ -11,7 +11,7 @@ import { t as DroppedError } from "./middleware-ChKM1zuu.mjs";
11
11
  function board(options = {}) {
12
12
  return createBoard(options, {
13
13
  http: () => http(),
14
- sync: () => import("./engine-ROa7-IQY.mjs"),
14
+ sync: () => import("./engine-Bfe6pcXb.mjs"),
15
15
  serving: null
16
16
  });
17
17
  }
@@ -1,5 +1,5 @@
1
1
  import { r as duration, t as BoardError } from "./client-D0Px7KzF.mjs";
2
2
  import { t as HttpError } from "./errors-CusN4x2l.mjs";
3
3
  import { t as DroppedError } from "./middleware-ChKM1zuu.mjs";
4
- import { t as board } from "./server-Bphio_82.mjs";
4
+ import { t as board } from "./server-DMNfPSAZ.mjs";
5
5
  export { BoardError, DroppedError, HttpError, board, duration };