@lantharos/pult 0.0.0-stage → 0.9.0

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/index.mjs ADDED
@@ -0,0 +1,1044 @@
1
+ import { A as mapBlocks, D as sign, E as NonceWindow, F as HOME, I as actionId, L as findActionDef, M as valuesIn, N as DEFAULT_STATES, O as verify, S as kindOf, T as refKey, a as collect, b as captureOrigin, d as Transport, f as b, g as isPendingAttachment, h as hasPendingAttachments, i as validate, k as eachBlock, m as fieldValues, n as reviveDates, o as findAction, p as bindActions, s as flag, t as ValidationError, u as PultError, v as view, w as HEADER, y as KIND } from "./schema-DuTVNptF.mjs";
2
+ //#region ../protocol/src/flags.ts
3
+ function rampAt(ramp, now = Date.now()) {
4
+ if (now >= ramp.end) return ramp.to;
5
+ if (now <= ramp.start) return ramp.from;
6
+ const elapsed = ramp.every ? Math.floor((now - ramp.start) / ramp.every) * ramp.every : now - ramp.start;
7
+ const value = ramp.from + (ramp.to - ramp.from) * elapsed / (ramp.end - ramp.start);
8
+ return ramp.whole ? Math.round(value) : value;
9
+ }
10
+ function currentRollout(state, now = Date.now()) {
11
+ return state.ramp?.of === "rollout" ? rampAt(state.ramp, now) : state.rollout;
12
+ }
13
+ function currentValue(state, now = Date.now()) {
14
+ return state.ramp?.of === "value" ? rampAt(state.ramp, now) : state.value;
15
+ }
16
+ function resolveFlag(name, state, context = {}, now = Date.now()) {
17
+ const { key, attributes = {} } = context;
18
+ if (key !== void 0 && Object.hasOwn(state.targets, key)) return state.targets[key];
19
+ const rule = state.rules.find((candidate) => candidate.conditions.every((condition) => holds(condition, attributes[condition.attribute])));
20
+ if (rule) return rule.rollout === null ? rule.value : share(name, key, rule.rollout);
21
+ const rollout = currentRollout(state, now);
22
+ if (rollout !== null) return share(name, key, rollout);
23
+ return currentValue(state, now);
24
+ }
25
+ function holds(condition, value) {
26
+ if (value === null || value === void 0) return condition.op === "is not";
27
+ switch (condition.op) {
28
+ case "is": return condition.values.includes(value);
29
+ case "is not": return !condition.values.includes(value);
30
+ case "above": return typeof value === "number" && value > Number(condition.values[0]);
31
+ case "below": return typeof value === "number" && value < Number(condition.values[0]);
32
+ }
33
+ }
34
+ function share(name, key, rollout) {
35
+ return key === void 0 ? rollout >= 100 : bucket(name, key) < rollout;
36
+ }
37
+ function bucket(name, key) {
38
+ let hash = 2166136261;
39
+ for (const char of `${name}:${key}`) {
40
+ hash ^= char.codePointAt(0);
41
+ hash = Math.imul(hash, 16777619);
42
+ }
43
+ return (hash >>> 0) % 1e4 / 100;
44
+ }
45
+ //#endregion
46
+ //#region src/client/runtime.ts
47
+ const onWorkers = globalThis.navigator?.userAgent === "Cloudflare-Workers";
48
+ //#endregion
49
+ //#region src/client/batch.ts
50
+ const FLUSH_MS = 8;
51
+ const MAX_ITEMS = 200;
52
+ const MAX_BYTES = 15e5;
53
+ const schedule = onWorkers ? queueMicrotask : (flush) => setTimeout(flush, FLUSH_MS);
54
+ var Batcher = class {
55
+ transport;
56
+ sync;
57
+ #queue = [];
58
+ #bytes = 0;
59
+ #scheduled = false;
60
+ constructor(transport, sync) {
61
+ this.transport = transport;
62
+ this.sync = sync;
63
+ }
64
+ send(item) {
65
+ return new Promise((resolve, reject) => {
66
+ const json = JSON.stringify(item);
67
+ const size = utf8Length(json);
68
+ if (this.#bytes + size > MAX_BYTES) this.#flush();
69
+ this.#queue.push({
70
+ json,
71
+ size,
72
+ resolve,
73
+ reject
74
+ });
75
+ this.#bytes += size;
76
+ if (this.#queue.length >= MAX_ITEMS) this.#flush();
77
+ else if (!this.#scheduled) {
78
+ this.#scheduled = true;
79
+ schedule(() => {
80
+ this.#scheduled = false;
81
+ this.#flush();
82
+ });
83
+ }
84
+ });
85
+ }
86
+ #flush() {
87
+ const batch = this.#queue;
88
+ this.#queue = [];
89
+ this.#bytes = 0;
90
+ if (batch.length) this.#post(batch);
91
+ }
92
+ async #post(batch) {
93
+ const body = `{"items":[${batch.map((entry) => entry.json).join(",")}]}`;
94
+ try {
95
+ let response;
96
+ try {
97
+ response = await this.transport.json("POST", "/v1/items", body);
98
+ } catch (error) {
99
+ if (!(error instanceof PultError && error.code === "manifest_required")) throw error;
100
+ await this.sync();
101
+ response = await this.transport.json("POST", "/v1/items", body);
102
+ }
103
+ response.results.forEach((outcome, index) => {
104
+ const entry = batch[index];
105
+ if ("error" in outcome) entry.reject(new PultError(outcome.error, 400, outcome.code));
106
+ else entry.resolve(outcome);
107
+ });
108
+ } catch (error) {
109
+ for (const entry of batch) entry.reject(error);
110
+ }
111
+ }
112
+ };
113
+ function utf8Length(text) {
114
+ let bytes = text.length;
115
+ for (let index = 0; index < text.length; index++) {
116
+ const code = text.charCodeAt(index);
117
+ if (code >= 128) bytes += code < 2048 || code >= 55296 && code <= 57343 ? 1 : 2;
118
+ }
119
+ return bytes;
120
+ }
121
+ //#endregion
122
+ //#region src/define/inbox/inbox.ts
123
+ function inbox(name, options) {
124
+ const states = options.states ?? DEFAULT_STATES;
125
+ const closed = options.closed ?? (options.states ? states.slice(1) : ["handled"]);
126
+ for (const state of closed) if (!states.includes(state)) throw new Error(`Inbox "${name}" closes on "${state}", which isn't one of its states`);
127
+ for (const state of Object.keys(options.sla ?? {})) if (!states.includes(state)) throw new Error(`Inbox "${name}" has a response time for "${state}", which isn't one of its states`);
128
+ return {
129
+ [KIND]: "inbox",
130
+ origin: captureOrigin(),
131
+ name,
132
+ options,
133
+ states,
134
+ closed
135
+ };
136
+ }
137
+ function liveView(title, data, record) {
138
+ return {
139
+ title,
140
+ data,
141
+ record,
142
+ occurrences: { live: "occurrences" },
143
+ users: { live: "users" },
144
+ firstSeen: { live: "firstSeen" },
145
+ lastSeen: { live: "lastSeen" },
146
+ due: { live: "due" }
147
+ };
148
+ }
149
+ //#endregion
150
+ //#region src/define/inbox/triage.ts
151
+ function noul(prompt, criteria) {
152
+ return {
153
+ type: "noul",
154
+ prompt,
155
+ ...criteria ? { criteria } : {}
156
+ };
157
+ }
158
+ function choice(options, prompt) {
159
+ return {
160
+ type: "choice",
161
+ prompt,
162
+ options: Array.isArray(options) ? Object.fromEntries(options.map((option) => [option, option])) : options
163
+ };
164
+ }
165
+ function score(levels, prompt) {
166
+ return {
167
+ type: "score",
168
+ prompt,
169
+ levels: [...levels]
170
+ };
171
+ }
172
+ function triage(options) {
173
+ return {
174
+ instructions: options.instructions ?? "",
175
+ questions: options.questions,
176
+ rules: options.rules ?? []
177
+ };
178
+ }
179
+ const setState = (state) => ({
180
+ type: "state",
181
+ state
182
+ });
183
+ const tag = (name) => ({
184
+ type: "tag",
185
+ tag: name
186
+ });
187
+ const run = (action, input) => ({
188
+ type: "run",
189
+ action: action.id,
190
+ input
191
+ });
192
+ function decide(rules, answers) {
193
+ const index = rules.findIndex((rule) => rule.when(answers));
194
+ if (index === -1) return null;
195
+ const then = rules[index].then;
196
+ return {
197
+ rule: index,
198
+ decisions: Array.isArray(then) ? then : [then]
199
+ };
200
+ }
201
+ //#endregion
202
+ //#region src/define/resource.ts
203
+ function action(name, options) {
204
+ return {
205
+ [KIND]: "action",
206
+ origin: captureOrigin(),
207
+ options,
208
+ resource: null,
209
+ name,
210
+ id: name
211
+ };
212
+ }
213
+ const identity = (value) => value;
214
+ function resource(name, options) {
215
+ const origin = captureOrigin();
216
+ const actions = { "*": { id: actionId(name, "*") } };
217
+ for (const [key, action] of Object.entries(options.actions?.(identity) ?? {})) actions[key] = {
218
+ [KIND]: "action",
219
+ origin,
220
+ options: action,
221
+ resource: name,
222
+ name: key,
223
+ id: actionId(name, key)
224
+ };
225
+ return {
226
+ [KIND]: "resource",
227
+ origin,
228
+ name,
229
+ options,
230
+ actions
231
+ };
232
+ }
233
+ function recordId(resource, record) {
234
+ return resource.options.id ? resource.options.id(record) : String(record.id);
235
+ }
236
+ //#endregion
237
+ //#region src/client/records.ts
238
+ function resourceOf(definitions, name) {
239
+ const resource = definitions.resources.get(name);
240
+ if (!resource) throw new Error(`Unknown resource "${name}"`);
241
+ return resource;
242
+ }
243
+ async function target(definitions, action, id) {
244
+ if (action.resource === null) return null;
245
+ const resource = resourceOf(definitions, action.resource);
246
+ const record = id === null ? null : await resource.options.get(id);
247
+ if (!record) throw new Error(`${resource.name} ${id} doesn't exist`);
248
+ return record;
249
+ }
250
+ function found(resource, record) {
251
+ return {
252
+ id: recordId(resource, record),
253
+ title: resource.options.title(record),
254
+ subtitle: resource.options.subtitle?.(record)
255
+ };
256
+ }
257
+ function listed(resource, record) {
258
+ return {
259
+ ...found(resource, record),
260
+ fields: fieldValues(resource.options.fields?.(record) ?? {})
261
+ };
262
+ }
263
+ async function find(definitions, request) {
264
+ const resource = resourceOf(definitions, request.resource);
265
+ if (!resource.options.find) return [];
266
+ return (await resource.options.find(request.query, request.limit)).slice(0, request.limit).map((record) => found(resource, record));
267
+ }
268
+ async function get(definitions, request, upload) {
269
+ const resource = resourceOf(definitions, request.resource);
270
+ const record = await resource.options.get(request.id);
271
+ if (!record) return null;
272
+ const blocks = resource.options.render ? await resource.options.render(record) : [];
273
+ return {
274
+ ...found(resource, record),
275
+ url: resource.options.url?.(record),
276
+ blocks: await settle(definitions, await upload(blocks), {
277
+ resource,
278
+ record
279
+ })
280
+ };
281
+ }
282
+ async function list(definitions, request) {
283
+ const resource = resourceOf(definitions, request.resource);
284
+ const options = resource.options.list;
285
+ if (!options) throw new Error(`${resource.name} can't be listed`);
286
+ const filters = options.filters ? await validate(options.filters, request.filters) : {};
287
+ const page = await options.run({
288
+ query: request.query,
289
+ filters,
290
+ cursor: request.cursor,
291
+ limit: request.limit
292
+ });
293
+ return {
294
+ records: page.records.slice(0, request.limit).map((record) => listed(resource, record)),
295
+ cursor: page.cursor ?? null
296
+ };
297
+ }
298
+ async function related(definitions, request) {
299
+ const resource = resourceOf(definitions, request.resource);
300
+ const relation = resource.options.related?.[request.relation];
301
+ if (!relation) throw new Error(`${resource.name} has no relation "${request.relation}"`);
302
+ const record = await resource.options.get(request.id);
303
+ if (!record) return [];
304
+ const target = resourceOf(definitions, relation.resource);
305
+ return (await relation.list(record, request.limit)).slice(0, request.limit).map((entry) => listed(target, entry));
306
+ }
307
+ async function choices(definitions, request) {
308
+ const action = findAction(definitions, request.action);
309
+ if (!action) throw new Error(`Unknown action "${request.action}"`);
310
+ const record = await target(definitions, action, request.record).catch(() => void 0);
311
+ if (record === void 0) return {};
312
+ const entries = Object.entries(action.options.choices ?? {});
313
+ const resolved = await Promise.all(entries.map(async ([field, options]) => [field, (await options(record)).map(toChoice)]));
314
+ return Object.fromEntries(resolved);
315
+ }
316
+ function toChoice(choice) {
317
+ return typeof choice === "string" ? {
318
+ value: choice,
319
+ label: choice
320
+ } : choice;
321
+ }
322
+ async function titles(definitions, request) {
323
+ const resolved = await Promise.all(request.refs.map(async (ref) => {
324
+ const resource = definitions.resources.get(ref.resource);
325
+ const record = resource && await resource.options.get(ref.id);
326
+ return record ? [refKey(ref), resource.options.title(record)] : null;
327
+ }));
328
+ return Object.fromEntries(resolved.filter((entry) => entry !== null));
329
+ }
330
+ async function available(definitions, request) {
331
+ const entries = await Promise.all(request.refs.map(async (ref) => {
332
+ const resource = definitions.resources.get(ref.resource);
333
+ const record = resource && await resource.options.get(ref.id);
334
+ return resource && record ? [[refKey(ref), actionsFor(resource, record)]] : [];
335
+ }));
336
+ return Object.fromEntries(entries.flat());
337
+ }
338
+ function actionsFor(resource, record) {
339
+ const actions = {};
340
+ for (const [name, action] of Object.entries(resource.actions)) {
341
+ if (kindOf(action) !== "action") continue;
342
+ const { when, defaults } = action.options;
343
+ if (when && !when(record)) continue;
344
+ actions[name] = defaults ? JSON.parse(JSON.stringify(defaults(record))) : null;
345
+ }
346
+ return actions;
347
+ }
348
+ async function settle(definitions, blocks, scope) {
349
+ const bound = scope ? bindActions(blocks, scope.resource, recordId(scope.resource, scope.record)) : blocks;
350
+ const loaded = /* @__PURE__ */ new Map();
351
+ if (scope) loaded.set(refKey({
352
+ resource: scope.resource.name,
353
+ id: recordId(scope.resource, scope.record)
354
+ }), Promise.resolve(scope.record));
355
+ const load = (resource, id) => {
356
+ const key = refKey({
357
+ resource: resource.name,
358
+ id
359
+ });
360
+ if (!loaded.has(key)) loaded.set(key, Promise.resolve(resource.options.get(id)));
361
+ return loaded.get(key);
362
+ };
363
+ const verdict = async ({ action: id, record: target }) => {
364
+ const action = findAction(definitions, id);
365
+ if (!action) return { applies: true };
366
+ const record = action.resource && target !== null ? await load(resourceOf(definitions, action.resource), target) : null;
367
+ if (action.resource && target !== null && !record) return { applies: false };
368
+ if (record && action.options.when && !action.options.when(record)) return { applies: false };
369
+ const defaults = action.options.input ? action.options.defaults?.(record) : void 0;
370
+ return {
371
+ applies: true,
372
+ ...defaults && { defaults: JSON.parse(JSON.stringify(defaults)) }
373
+ };
374
+ };
375
+ const targets = /* @__PURE__ */ new Map();
376
+ const keyOf = (target) => `${target.action}@${target.record ?? ""}`;
377
+ const visit = (target) => {
378
+ if (!targets.has(keyOf(target))) targets.set(keyOf(target), verdict(target));
379
+ };
380
+ eachBlock(bound, (block) => {
381
+ if (block.type === "actions") for (const button of block.actions) visit(button);
382
+ if (block.type === "form") visit(block.form);
383
+ for (const value of valuesIn(block)) if (value && typeof value === "object" && "edit" in value) visit(value.edit);
384
+ });
385
+ if (!targets.size) return bound;
386
+ const verdicts = new Map(await Promise.all([...targets].map(async ([key, pending]) => [key, await pending])));
387
+ const editable = (value) => value && typeof value === "object" && "edit" in value && !verdicts.get(keyOf(value.edit))?.applies ? value.edit.value : value;
388
+ const settled = (button) => {
389
+ const found = verdicts.get(keyOf(button));
390
+ if (!found.applies) return null;
391
+ return found.defaults ? {
392
+ ...button,
393
+ input: {
394
+ ...found.defaults,
395
+ ...button.input
396
+ }
397
+ } : button;
398
+ };
399
+ return mapBlocks(bound, (block) => {
400
+ if (block.type === "actions") {
401
+ const actions = block.actions.flatMap((button) => settled(button) ?? []);
402
+ return actions.length ? {
403
+ ...block,
404
+ actions
405
+ } : null;
406
+ }
407
+ if (block.type === "form") {
408
+ const form = settled(block.form);
409
+ return form ? {
410
+ ...block,
411
+ form
412
+ } : null;
413
+ }
414
+ if (block.type === "fields") return {
415
+ ...block,
416
+ fields: block.fields.map((field) => ({
417
+ ...field,
418
+ value: editable(field.value)
419
+ }))
420
+ };
421
+ if (block.type === "stats") return {
422
+ ...block,
423
+ stats: block.stats.map((stat) => ({
424
+ ...stat,
425
+ value: editable(stat.value)
426
+ }))
427
+ };
428
+ if (block.type === "table") return {
429
+ ...block,
430
+ rows: block.rows.map((row) => row.map(editable))
431
+ };
432
+ return block;
433
+ });
434
+ }
435
+ //#endregion
436
+ //#region src/client/actions.ts
437
+ const ignoreProgress = async () => {};
438
+ async function runAction(definitions, manifest, { id, request, actor }, upload, jobs) {
439
+ const action = findAction(definitions, request.action);
440
+ if (!action) throw new Error(`Unknown action "${request.action}"`);
441
+ const def = findActionDef(manifest, request.action);
442
+ const record = await target(definitions, action, request.record);
443
+ if (action.options.when && !action.options.when(record)) {
444
+ const title = resourceOf(definitions, action.resource).options.title(record);
445
+ throw new Error(`${def?.label ?? action.name} doesn't apply to ${title} right now`);
446
+ }
447
+ const raw = reviveDates(def?.input ?? null, request.input);
448
+ const input = action.options.input ? await validate(action.options.input, raw) : {};
449
+ const background = Boolean(action.options.background) && !request.preview;
450
+ const progress = background ? (fraction, note) => jobs.report(id, {
451
+ progress: fraction,
452
+ note
453
+ }) : ignoreProgress;
454
+ const context = {
455
+ record,
456
+ input,
457
+ actor,
458
+ item: request.item,
459
+ progress
460
+ };
461
+ if (request.preview) {
462
+ if (!action.options.preview) return {};
463
+ return { blocks: await upload(await action.options.preview(context)) };
464
+ }
465
+ if (!background) return toResult(await action.options.run(context), upload);
466
+ jobs.defer((async () => {
467
+ try {
468
+ await jobs.report(id, { result: await toResult(await action.options.run(context), upload) });
469
+ } catch (error) {
470
+ await jobs.report(id, { error: error.message });
471
+ }
472
+ })());
473
+ return { running: true };
474
+ }
475
+ async function toResult(value, upload) {
476
+ if (!value) return {};
477
+ if (typeof value === "string") return { message: value };
478
+ return {
479
+ message: value.message,
480
+ blocks: value.blocks && await upload(value.blocks)
481
+ };
482
+ }
483
+ //#endregion
484
+ //#region src/client/dispatch.ts
485
+ var Dispatcher = class {
486
+ project;
487
+ upload;
488
+ report;
489
+ #definitions = null;
490
+ constructor(project, upload, report) {
491
+ this.project = project;
492
+ this.upload = upload;
493
+ this.report = report;
494
+ }
495
+ definitions() {
496
+ this.#definitions ??= Promise.all(Object.values(this.project.modules).map((load) => load())).then(collect).catch((error) => {
497
+ this.#definitions = null;
498
+ throw error;
499
+ });
500
+ return this.#definitions;
501
+ }
502
+ async handle(envelope, defer = () => {}) {
503
+ const { request } = envelope;
504
+ const definitions = await this.definitions();
505
+ switch (request.kind) {
506
+ case "ping": return null;
507
+ case "action": return runAction(definitions, this.project.manifest, envelope, this.upload, {
508
+ report: this.report,
509
+ defer
510
+ });
511
+ case "find": return find(definitions, request);
512
+ case "get": return get(definitions, request, this.upload);
513
+ case "list": return list(definitions, request);
514
+ case "related": return related(definitions, request);
515
+ case "choices": return choices(definitions, request);
516
+ case "titles": return titles(definitions, request);
517
+ case "available": return available(definitions, request);
518
+ case "page": return this.#page(definitions, envelope);
519
+ case "render": return renderItem(this.#inbox(definitions, request.inbox), request, this.upload);
520
+ case "triage": return this.#triage(definitions, request);
521
+ case "replay": return this.#replay(definitions, request);
522
+ case "state": return this.#state(definitions, envelope);
523
+ case "reply": return this.#reply(definitions, envelope);
524
+ }
525
+ }
526
+ async #page(definitions, { request, actor }) {
527
+ const page = definitions.pages.get(request.page);
528
+ if (!page) throw new Error(`Unknown page "${request.page}"`);
529
+ const { params, render } = page.options;
530
+ const parsed = params ? await validate(params, request.params) : {};
531
+ const blocks = typeof render === "function" ? await render({
532
+ params: parsed,
533
+ actor
534
+ }) : render;
535
+ return settle(definitions, await this.upload(blocks), null);
536
+ }
537
+ #inbox(definitions, name) {
538
+ const inbox = definitions.inboxes.get(name);
539
+ if (!inbox) throw new Error(`Unknown inbox "${name}"`);
540
+ return inbox;
541
+ }
542
+ async #state(definitions, { request, actor }) {
543
+ const inbox = this.#inbox(definitions, request.inbox);
544
+ const item = {
545
+ id: request.item,
546
+ title: request.title,
547
+ record: request.record,
548
+ data: request.data
549
+ };
550
+ await inbox.options.onState?.({
551
+ item,
552
+ from: request.from,
553
+ to: request.to,
554
+ actor
555
+ });
556
+ return null;
557
+ }
558
+ async #reply(definitions, { request, actor }) {
559
+ const reply = this.#inbox(definitions, request.inbox).options.reply;
560
+ if (!reply) throw new Error(`${request.inbox} can't send email`);
561
+ const { item, to, subject, text, thread } = request;
562
+ await reply.send({
563
+ item,
564
+ to,
565
+ subject,
566
+ text,
567
+ thread,
568
+ actor
569
+ });
570
+ return null;
571
+ }
572
+ #replay(definitions, request) {
573
+ const rules = this.#inbox(definitions, request.inbox).options.triage?.rules ?? [];
574
+ return request.answers.map((answers) => decide(rules, answers));
575
+ }
576
+ #triage(definitions, request) {
577
+ const inbox = this.#inbox(definitions, request.inbox);
578
+ return inbox.options.triage ? decide(inbox.options.triage.rules, request.answers) : null;
579
+ }
580
+ };
581
+ async function renderItem(inbox, item, upload) {
582
+ const data = inbox.options.data ? await validate(inbox.options.data, item.data) : item.data;
583
+ const title = item.title || inbox.options.title?.(data) || inbox.options.label || inbox.name;
584
+ const view = liveView(title, data, item.record ?? null);
585
+ const blocks = inbox.options.render ? await inbox.options.render(view) : defaultBlocks(data);
586
+ return {
587
+ title,
588
+ record: item.record ?? void 0,
589
+ data,
590
+ blocks: bindActions(await upload(blocks), inbox.options.resource ?? null, item.record ?? null),
591
+ replyTo: inbox.options.reply?.to(view)
592
+ };
593
+ }
594
+ function defaultBlocks(data) {
595
+ if (!data || typeof data !== "object") return [];
596
+ const fields = Object.entries(data).filter(([, value]) => value !== null && typeof value !== "object").map(([label, value]) => ({
597
+ label,
598
+ value
599
+ }));
600
+ return fields.length ? [{
601
+ type: "fields",
602
+ fields
603
+ }] : [];
604
+ }
605
+ //#endregion
606
+ //#region src/client/flags.ts
607
+ const FRESH_MS = 1e4;
608
+ const MAX_PARSED = 256;
609
+ const EMPTY = {
610
+ version: -1,
611
+ flags: {}
612
+ };
613
+ var Flags = class {
614
+ transport;
615
+ #snapshot = null;
616
+ #checkedAt = 0;
617
+ #live = false;
618
+ #refreshing = null;
619
+ #parsed = /* @__PURE__ */ new Map();
620
+ #reads = /* @__PURE__ */ new Map();
621
+ constructor(transport) {
622
+ this.transport = transport;
623
+ }
624
+ receive(snapshot) {
625
+ this.#live = snapshot !== null;
626
+ if (snapshot) this.#store(snapshot);
627
+ }
628
+ async value(flag, context) {
629
+ this.#reads.set(flag.name, (this.#reads.get(flag.name) ?? 0) + 1);
630
+ const state = (await this.#current()).flags[flag.name];
631
+ const raw = state === void 0 ? void 0 : resolveFlag(flag.name, state, contextOf(context));
632
+ if (raw === void 0 || raw === null) return flag.options.default;
633
+ if (!flag.options.value) return typeof raw === "boolean" ? raw : flag.options.default;
634
+ return this.#validated(flag, raw);
635
+ }
636
+ takeReads() {
637
+ if (!this.#reads.size) return null;
638
+ const reads = Object.fromEntries(this.#reads);
639
+ this.#reads.clear();
640
+ return reads;
641
+ }
642
+ #store(snapshot) {
643
+ if (snapshot !== this.#snapshot) this.#parsed.clear();
644
+ this.#snapshot = snapshot;
645
+ this.#checkedAt = Date.now();
646
+ }
647
+ async #current() {
648
+ const fresh = this.#live || Date.now() - this.#checkedAt < FRESH_MS;
649
+ if (this.#snapshot && fresh) return this.#snapshot;
650
+ if (onWorkers) return this.#refresh();
651
+ this.#refreshing ??= this.#refresh().finally(() => {
652
+ this.#refreshing = null;
653
+ });
654
+ return this.#refreshing;
655
+ }
656
+ async #refresh() {
657
+ const since = this.#snapshot?.version ?? -1;
658
+ const reads = this.takeReads();
659
+ const counted = reads ? `&reads=${encodeURIComponent(Object.entries(reads).map(([name, count]) => `${name}:${count}`).join(","))}` : "";
660
+ try {
661
+ const answer = await this.transport.request("GET", `/v1/flags?since=${since}${counted}`);
662
+ if ("unchanged" in answer) this.#checkedAt = Date.now();
663
+ else this.#store(answer);
664
+ } catch {
665
+ this.#checkedAt = Date.now();
666
+ }
667
+ return this.#snapshot ?? EMPTY;
668
+ }
669
+ #validated(flag, raw) {
670
+ let values = this.#parsed.get(flag.name);
671
+ if (!values) {
672
+ values = /* @__PURE__ */ new Map();
673
+ this.#parsed.set(flag.name, values);
674
+ }
675
+ let parsed = values.get(raw);
676
+ if (!parsed) {
677
+ if (values.size >= MAX_PARSED) values.clear();
678
+ parsed = validate(flag.options.value, raw).catch(() => flag.options.default);
679
+ values.set(raw, parsed);
680
+ }
681
+ return parsed;
682
+ }
683
+ };
684
+ function contextOf(context) {
685
+ if (context === void 0 || typeof context === "string") return { key: context };
686
+ const { key, ...attributes } = context;
687
+ return {
688
+ key,
689
+ attributes
690
+ };
691
+ }
692
+ //#endregion
693
+ //#region src/client/socket.ts
694
+ const HEARTBEAT_MS = 25e3;
695
+ const SILENCE_MS = 2 * HEARTBEAT_MS;
696
+ const MAX_BACKOFF_MS = 3e4;
697
+ function connect({ transport, nonces, version, onRequest, onSyncManifest, onFlags, reads, onStatus }) {
698
+ let socket = null;
699
+ let heartbeat;
700
+ let retry;
701
+ let attempt = 0;
702
+ let closed = false;
703
+ const send = (message) => socket?.send(JSON.stringify(message));
704
+ const respond = async (response) => {
705
+ const body = JSON.stringify(response);
706
+ send({
707
+ type: "response",
708
+ signature: await sign(transport.key.secret, "response", body),
709
+ body
710
+ });
711
+ };
712
+ const handle = async (message) => {
713
+ if (message.type === "welcome") {
714
+ attempt = 0;
715
+ onStatus?.("connected");
716
+ const manifest = message.syncManifest ? onSyncManifest(message.environment) : null;
717
+ if (manifest) send(manifest);
718
+ return;
719
+ }
720
+ if (message.type === "flags") {
721
+ onFlags(message.snapshot);
722
+ return;
723
+ }
724
+ if (await verify(transport.key.secret, "request", message.body, message.signature) || !nonces.claim(message.signature.nonce)) return;
725
+ const envelope = JSON.parse(message.body);
726
+ try {
727
+ await respond({
728
+ id: envelope.id,
729
+ ok: true,
730
+ result: await onRequest(envelope) ?? null
731
+ });
732
+ } catch (error) {
733
+ await respond({
734
+ id: envelope.id,
735
+ ok: false,
736
+ error: { message: error.message }
737
+ });
738
+ }
739
+ };
740
+ const stopHeartbeat = () => {
741
+ if (heartbeat) clearInterval(heartbeat);
742
+ };
743
+ const reconnect = () => {
744
+ stopHeartbeat();
745
+ onFlags(null);
746
+ onStatus?.("disconnected");
747
+ if (closed) return;
748
+ const delay = Math.min(MAX_BACKOFF_MS, 500 * 2 ** attempt++) * (.5 + Math.random() / 2);
749
+ retry = setTimeout(open, delay);
750
+ };
751
+ const open = async () => {
752
+ const signature = await sign(transport.key.secret, "connect", version);
753
+ if (closed) return;
754
+ const params = new URLSearchParams({
755
+ environment: transport.key.environment,
756
+ version,
757
+ ...signature
758
+ });
759
+ const current = new WebSocket(`${transport.url.replace(/^http/, "ws")}/v1/connect?${params}`);
760
+ let heardAt = Date.now();
761
+ let lost = false;
762
+ const lose = () => {
763
+ if (lost) return;
764
+ lost = true;
765
+ current.close();
766
+ reconnect();
767
+ };
768
+ socket = current;
769
+ current.addEventListener("message", (event) => {
770
+ heardAt = Date.now();
771
+ if (event.data !== "pong") handle(JSON.parse(String(event.data)));
772
+ });
773
+ current.addEventListener("open", () => {
774
+ heardAt = Date.now();
775
+ heartbeat = setInterval(() => {
776
+ if (Date.now() - heardAt > SILENCE_MS) return lose();
777
+ current.send("ping");
778
+ const counted = reads();
779
+ if (counted) send({
780
+ type: "reads",
781
+ reads: counted
782
+ });
783
+ }, HEARTBEAT_MS);
784
+ });
785
+ current.addEventListener("close", lose);
786
+ };
787
+ open();
788
+ return { close() {
789
+ closed = true;
790
+ if (retry) clearTimeout(retry);
791
+ stopHeartbeat();
792
+ socket?.close();
793
+ } };
794
+ }
795
+ //#endregion
796
+ //#region src/client/upload.ts
797
+ function uploader(transport) {
798
+ const attach = async ({ name, contentType, data }) => {
799
+ const bytes = await bytesOf(data);
800
+ const { key } = await transport.upload(name, contentType, bytes);
801
+ return {
802
+ type: "attachment",
803
+ name,
804
+ key,
805
+ contentType,
806
+ size: bytes.byteLength
807
+ };
808
+ };
809
+ const walk = (blocks) => Promise.all(blocks.map(async (block) => {
810
+ if (isPendingAttachment(block)) return attach(block);
811
+ if (block.type === "columns") return {
812
+ type: "columns",
813
+ columns: await Promise.all(block.columns.map(walk))
814
+ };
815
+ return block;
816
+ }));
817
+ return async (blocks) => hasPendingAttachments(blocks) ? walk(blocks) : blocks;
818
+ }
819
+ async function bytesOf(data) {
820
+ if (data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
821
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
822
+ return data;
823
+ }
824
+ //#endregion
825
+ //#region src/client/pult.ts
826
+ function createPult({ key = env("PULT_KEY"), project, url = env("PULT_URL") ?? "https://app.pult.sh", fetch = globalThis.fetch }) {
827
+ if (!key) throw new Error("Pult needs a key: pass `key` or set PULT_KEY");
828
+ const transport = new Transport(key, url, (input, init) => fetch(input, init));
829
+ const upload = uploader(transport);
830
+ const nonces = new NonceWindow();
831
+ const report = async (job, update) => {
832
+ await transport.request("POST", `/v1/jobs/${job}`, update);
833
+ };
834
+ const dispatcher = new Dispatcher(project, upload, report);
835
+ const { manifest } = project;
836
+ const deploy = () => transport.request("POST", "/v1/manifest", manifest);
837
+ const sync = () => transport.request("POST", "/v1/manifest/sync", manifest);
838
+ const batcher = new Batcher(transport, sync);
839
+ const flags = new Flags(transport);
840
+ async function send(inbox, item) {
841
+ const rendered = await renderItem(inbox, item, upload);
842
+ return batcher.send({
843
+ inbox: inbox.name,
844
+ fingerprint: item.fingerprint,
845
+ tags: item.tags,
846
+ suggest: item.suggest?.map(({ action, ...rest }) => ({
847
+ action: action.id,
848
+ ...rest
849
+ })),
850
+ version: manifest.version,
851
+ ...rendered
852
+ });
853
+ }
854
+ async function log(resource, id, text, options = {}) {
855
+ await transport.request("POST", "/v1/history", {
856
+ resource: resource.name,
857
+ id,
858
+ text,
859
+ by: options.by,
860
+ blocks: options.blocks && await upload(options.blocks)
861
+ });
862
+ }
863
+ async function receive(message) {
864
+ try {
865
+ return await transport.request("POST", "/v1/messages", message);
866
+ } catch (error) {
867
+ if (error instanceof PultError && error.code === "no_thread") return null;
868
+ throw error;
869
+ }
870
+ }
871
+ function listen(options = {}) {
872
+ return connect({
873
+ transport,
874
+ nonces,
875
+ version: manifest.version,
876
+ onRequest: (envelope) => dispatcher.handle(envelope),
877
+ onSyncManifest: (environment) => environment === manifest.sync ? {
878
+ type: "manifest",
879
+ manifest
880
+ } : null,
881
+ onFlags: (snapshot) => flags.receive(snapshot),
882
+ reads: () => flags.takeReads(),
883
+ onStatus: options.onStatus
884
+ });
885
+ }
886
+ function handler() {
887
+ return async (request, context) => {
888
+ const body = await request.text();
889
+ const signature = signedHeaders(request.headers);
890
+ const failure = signature && await verify(transport.key.secret, "request", body, signature);
891
+ if (!signature || failure || !nonces.claim(signature.nonce)) return Response.json({ error: "invalid signature" }, { status: 401 });
892
+ const envelope = JSON.parse(body);
893
+ let response;
894
+ try {
895
+ const result = await dispatcher.handle(envelope, (work) => context?.waitUntil(work));
896
+ response = {
897
+ id: envelope.id,
898
+ ok: true,
899
+ result: result ?? null
900
+ };
901
+ } catch (error) {
902
+ response = {
903
+ id: envelope.id,
904
+ ok: false,
905
+ error: { message: error.message }
906
+ };
907
+ }
908
+ const payload = JSON.stringify(response);
909
+ const signed = await sign(transport.key.secret, "response", payload);
910
+ return new Response(payload, { headers: {
911
+ "content-type": "application/json",
912
+ [HEADER.timestamp]: signed.timestamp,
913
+ [HEADER.nonce]: signed.nonce,
914
+ [HEADER.signature]: signed.signature
915
+ } });
916
+ };
917
+ }
918
+ const status = { async set(component, state) {
919
+ if (!manifest.status?.components[component]) throw new Error(`Your status page has no component "${component}"`);
920
+ await transport.request("POST", "/v1/status", {
921
+ component,
922
+ state
923
+ });
924
+ } };
925
+ function flag(definition, context) {
926
+ return flags.value(definition, context);
927
+ }
928
+ return {
929
+ send,
930
+ log,
931
+ receive,
932
+ flag,
933
+ status,
934
+ listen,
935
+ handler,
936
+ deploy
937
+ };
938
+ }
939
+ function signedHeaders(headers) {
940
+ const timestamp = headers.get(HEADER.timestamp);
941
+ const nonce = headers.get(HEADER.nonce);
942
+ const signature = headers.get(HEADER.signature);
943
+ return timestamp && nonce && signature ? {
944
+ timestamp,
945
+ nonce,
946
+ signature
947
+ } : null;
948
+ }
949
+ function env(name) {
950
+ return globalThis.process?.env[name];
951
+ }
952
+ //#endregion
953
+ //#region src/define/config.ts
954
+ function defineConfig(options) {
955
+ const environments = options.environments ?? [
956
+ "dev",
957
+ "staging",
958
+ "prod"
959
+ ];
960
+ const sync = options.sync ?? environments[0];
961
+ if (!environments.includes(sync)) throw new Error(`sync is "${sync}", which isn't one of your environments (${environments.join(", ")})`);
962
+ return {
963
+ [KIND]: "config",
964
+ origin: captureOrigin(),
965
+ options: {
966
+ delivery: { mode: "socket" },
967
+ ...options,
968
+ environments,
969
+ sync
970
+ }
971
+ };
972
+ }
973
+ //#endregion
974
+ //#region src/define/console/page.ts
975
+ function page(name, options) {
976
+ return {
977
+ [KIND]: "page",
978
+ origin: captureOrigin(),
979
+ name,
980
+ options
981
+ };
982
+ }
983
+ function home(options) {
984
+ const resolved = Array.isArray(options) || typeof options === "function" ? { render: options } : options;
985
+ return {
986
+ [KIND]: "page",
987
+ origin: captureOrigin(),
988
+ name: HOME,
989
+ options: {
990
+ label: "Home",
991
+ ...resolved
992
+ }
993
+ };
994
+ }
995
+ //#endregion
996
+ //#region src/define/console/role.ts
997
+ function role(name, options) {
998
+ return {
999
+ [KIND]: "role",
1000
+ origin: captureOrigin(),
1001
+ name,
1002
+ options
1003
+ };
1004
+ }
1005
+ //#endregion
1006
+ //#region src/define/console/space.ts
1007
+ function space(name, options) {
1008
+ const { home } = options;
1009
+ return {
1010
+ [KIND]: "space",
1011
+ origin: captureOrigin(),
1012
+ name,
1013
+ options,
1014
+ home: home === void 0 ? null : kindOf(home) === "page" ? home : page(name, {
1015
+ label: options.label,
1016
+ icon: options.icon,
1017
+ render: home
1018
+ })
1019
+ };
1020
+ }
1021
+ //#endregion
1022
+ //#region src/define/controls/status.ts
1023
+ function status(options) {
1024
+ return {
1025
+ [KIND]: "status",
1026
+ origin: captureOrigin(),
1027
+ options
1028
+ };
1029
+ }
1030
+ //#endregion
1031
+ //#region src/define/inbox/notify.ts
1032
+ function slack(channel) {
1033
+ return { channel: channel.replace(/^#/, "") };
1034
+ }
1035
+ function notify(name, options) {
1036
+ return {
1037
+ [KIND]: "notify",
1038
+ origin: captureOrigin(),
1039
+ name,
1040
+ options
1041
+ };
1042
+ }
1043
+ //#endregion
1044
+ export { PultError, ValidationError, action, b, choice, createPult, defineConfig, flag, home, inbox, notify, noul, page, resource, role, run, score, setState, slack, space, status, tag, triage, view };