@marktiderman/genesis-forms 0.9.0 → 0.9.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.
@@ -1,4 +1,4 @@
1
- import { J as Json, F as FormDefinition, a as FieldBlock, C as Condition } from './types-C0pUfew3.cjs';
1
+ import { J as Json, F as FormDefinition, a as FieldBlock, C as Condition } from './types-BBDa8oXu.cjs';
2
2
 
3
3
  /**
4
4
  * The contract every destination keeps. A destination sends one checked
@@ -733,30 +733,6 @@ declare function sealSecret(key: WebCryptoKey, plaintext: string, aad: string):
733
733
  */
734
734
  declare function openSecret(key: WebCryptoKey, sealed: string, aad: string): Promise<string>;
735
735
 
736
- /**
737
- * The outbox runner: claim a batch of due deliveries, send each to its
738
- * destination, and record what happened. The store is the app's (a SQL
739
- * outbox, a queue, a KV list): it hands over claimed deliveries and takes
740
- * back outcomes, and decides how claims and leases work. The runner owns the
741
- * rest: the destination's condition (`config.when`), batching deliveries to
742
- * one destination into one request where the destination can, keeping its
743
- * state, and turning each result into an outcome with a retry time.
744
- *
745
- * Outcomes:
746
- * - `sent`: delivered (or already there, on a redelivery).
747
- * - `filtered`: the submission doesn't match the destination's condition; nothing was sent, nothing to retry.
748
- * - `skipped`: the destination deliberately sent nothing (`notSent`); never retried.
749
- * - `retry`: a retryable failure. With the service's own Retry-After it is
750
- * deferred without counting an attempt (`counted: false`); otherwise it backs
751
- * off by attempt (`counted: true`), until `maxAttempts`.
752
- * - `failed`: permanent, an unknown kind, or out of attempts. With `disable:
753
- * true` the receiver asked to be switched off (a webhook's 410 Gone): the
754
- * app should disable the destination rather than keep sending to it.
755
- *
756
- * It never throws for a delivery's failure, and never logs answers, contact
757
- * details or secrets: only ids, kinds and the destinations' safe messages.
758
- */
759
-
760
736
  /** One claimed delivery, as the app's store hands it over. */
761
737
  interface ClaimedDelivery {
762
738
  /** The delivery's id: stable across its retries (the email idempotency key, the webhook-id). */
@@ -874,9 +850,9 @@ interface DeliveryRunner<Filter = undefined> {
874
850
  /** One claim, its sends and their records. Never rejects for a delivery's failure; a failing `claim` rejects. */
875
851
  run(options?: RunOptions<Filter>): Promise<DeliveryReport>;
876
852
  }
877
- declare const DEFAULT_MAX_ATTEMPTS = 8;
878
- declare const DEFAULT_MAX_DEFERRALS = 24;
879
- declare const DEFAULT_BATCH_SIZE = 25;
853
+ declare const DEFAULT_MAX_ATTEMPTS: number;
854
+ declare const DEFAULT_MAX_DEFERRALS: number;
855
+ declare const DEFAULT_BATCH_SIZE: number;
880
856
  /**
881
857
  * 5 minutes, quadrupling each attempt, at most 24 hours: 5m, 20m, 80m, 5h20m,
882
858
  * 21h20m, then 24h. Across the default 8 attempts that is about 3 days, as
@@ -884,7 +860,7 @@ declare const DEFAULT_BATCH_SIZE = 25;
884
860
  * deliveries that failed together (a receiver's outage) don't all come back
885
861
  * at once. `random` (default `Math.random`) is for tests.
886
862
  */
887
- declare function defaultBackoff(attempt: number, random?: () => number): number;
863
+ declare const defaultBackoff: (attempt: number, random?: () => number) => number;
888
864
  /** Turns a destination's result into an outcome for a delivery on its `attempt`. */
889
865
  declare function deliveryOutcome(result: DeliveryResult, attempt: number, options?: {
890
866
  now?: number | undefined;
@@ -1,4 +1,4 @@
1
- import { J as Json, F as FormDefinition, a as FieldBlock, C as Condition } from './types-C0pUfew3.js';
1
+ import { J as Json, F as FormDefinition, a as FieldBlock, C as Condition } from './types-BBDa8oXu.js';
2
2
 
3
3
  /**
4
4
  * The contract every destination keeps. A destination sends one checked
@@ -733,30 +733,6 @@ declare function sealSecret(key: WebCryptoKey, plaintext: string, aad: string):
733
733
  */
734
734
  declare function openSecret(key: WebCryptoKey, sealed: string, aad: string): Promise<string>;
735
735
 
736
- /**
737
- * The outbox runner: claim a batch of due deliveries, send each to its
738
- * destination, and record what happened. The store is the app's (a SQL
739
- * outbox, a queue, a KV list): it hands over claimed deliveries and takes
740
- * back outcomes, and decides how claims and leases work. The runner owns the
741
- * rest: the destination's condition (`config.when`), batching deliveries to
742
- * one destination into one request where the destination can, keeping its
743
- * state, and turning each result into an outcome with a retry time.
744
- *
745
- * Outcomes:
746
- * - `sent`: delivered (or already there, on a redelivery).
747
- * - `filtered`: the submission doesn't match the destination's condition; nothing was sent, nothing to retry.
748
- * - `skipped`: the destination deliberately sent nothing (`notSent`); never retried.
749
- * - `retry`: a retryable failure. With the service's own Retry-After it is
750
- * deferred without counting an attempt (`counted: false`); otherwise it backs
751
- * off by attempt (`counted: true`), until `maxAttempts`.
752
- * - `failed`: permanent, an unknown kind, or out of attempts. With `disable:
753
- * true` the receiver asked to be switched off (a webhook's 410 Gone): the
754
- * app should disable the destination rather than keep sending to it.
755
- *
756
- * It never throws for a delivery's failure, and never logs answers, contact
757
- * details or secrets: only ids, kinds and the destinations' safe messages.
758
- */
759
-
760
736
  /** One claimed delivery, as the app's store hands it over. */
761
737
  interface ClaimedDelivery {
762
738
  /** The delivery's id: stable across its retries (the email idempotency key, the webhook-id). */
@@ -874,9 +850,9 @@ interface DeliveryRunner<Filter = undefined> {
874
850
  /** One claim, its sends and their records. Never rejects for a delivery's failure; a failing `claim` rejects. */
875
851
  run(options?: RunOptions<Filter>): Promise<DeliveryReport>;
876
852
  }
877
- declare const DEFAULT_MAX_ATTEMPTS = 8;
878
- declare const DEFAULT_MAX_DEFERRALS = 24;
879
- declare const DEFAULT_BATCH_SIZE = 25;
853
+ declare const DEFAULT_MAX_ATTEMPTS: number;
854
+ declare const DEFAULT_MAX_DEFERRALS: number;
855
+ declare const DEFAULT_BATCH_SIZE: number;
880
856
  /**
881
857
  * 5 minutes, quadrupling each attempt, at most 24 hours: 5m, 20m, 80m, 5h20m,
882
858
  * 21h20m, then 24h. Across the default 8 attempts that is about 3 days, as
@@ -884,7 +860,7 @@ declare const DEFAULT_BATCH_SIZE = 25;
884
860
  * deliveries that failed together (a receiver's outage) don't all come back
885
861
  * at once. `random` (default `Math.random`) is for tests.
886
862
  */
887
- declare function defaultBackoff(attempt: number, random?: () => number): number;
863
+ declare const defaultBackoff: (attempt: number, random?: () => number) => number;
888
864
  /** Turns a destination's result into an outcome for a delivery on its `attempt`. */
889
865
  declare function deliveryOutcome(result: DeliveryResult, attempt: number, options?: {
890
866
  now?: number | undefined;
@@ -2093,7 +2093,7 @@ function padded(text) {
2093
2093
  return fromBase64(text + "=".repeat((4 - text.length % 4) % 4));
2094
2094
  }
2095
2095
 
2096
- // src/destinations/runner.ts
2096
+ // ../notifications/src/runner/index.ts
2097
2097
  var DEFAULT_MAX_ATTEMPTS = 8;
2098
2098
  var DEFAULT_MAX_DEFERRALS = 24;
2099
2099
  var DEFAULT_BATCH_SIZE = 25;
@@ -2102,8 +2102,11 @@ function defaultBackoff(attempt, random) {
2102
2102
  const r = typeof random === "function" ? random() : Math.random();
2103
2103
  return Math.round(nominal * (0.8 + 0.4 * Math.min(1, Math.max(0, r))));
2104
2104
  }
2105
- function deliveryOutcome(result, attempt, options = {}) {
2106
- if (result.ok) return result.notSent ? { status: "skipped", reason: result.notSent.slice(0, 500) } : { status: "sent" };
2105
+ function jobOutcome(result, attempt, options = {}) {
2106
+ if (result.ok) {
2107
+ if (result.notSent) return { status: "skipped", reason: result.notSent.slice(0, 500) };
2108
+ return result.alreadyThere ? { status: "sent", alreadyThere: true } : { status: "sent" };
2109
+ }
2107
2110
  const http = result.status !== void 0 ? { httpStatus: result.status } : {};
2108
2111
  if (result.disable) return { status: "failed", error: result.error, ...http, disable: true };
2109
2112
  if (!result.retryable) return { status: "failed", error: result.error, ...http };
@@ -2117,14 +2120,109 @@ function deliveryOutcome(result, attempt, options = {}) {
2117
2120
  const seconds = Math.max(1, Math.ceil((options.backoff ?? defaultBackoff)(attempt)));
2118
2121
  return { status: "retry", error: result.error, retryAt: new Date(now + seconds * 1e3), retryAfterSeconds: seconds, counted: true, ...http };
2119
2122
  }
2120
- function createDeliveryRunner(options) {
2123
+ function failureOf(error) {
2124
+ const e = error;
2125
+ if (e && typeof e === "object" && typeof e.retryable === "boolean" && typeof e.message === "string") {
2126
+ return {
2127
+ ok: false,
2128
+ retryable: e.disable === true ? false : e.retryable,
2129
+ error: e.message.slice(0, 500),
2130
+ ...typeof e.retryAfterSeconds === "number" ? { retryAfterSeconds: e.retryAfterSeconds } : {},
2131
+ ...typeof e.status === "number" ? { status: e.status } : {},
2132
+ ...e.disable === true ? { disable: true } : {}
2133
+ };
2134
+ }
2135
+ const timeout = e && typeof e === "object" && (e.name === "TimeoutError" || e.name === "AbortError");
2136
+ return { ok: false, retryable: true, error: timeout ? "Timed out" : "Unexpected error" };
2137
+ }
2138
+ function createRunner(options) {
2121
2139
  const log = (message) => {
2122
2140
  try {
2123
2141
  options.log?.(message);
2124
2142
  } catch {
2125
2143
  }
2126
2144
  };
2145
+ const label = (job) => options.label?.(job) ?? `Job ${job.id}`;
2146
+ const recordLabel = (job) => options.recordLabel?.(job) ?? `job ${job.id}`;
2127
2147
  const now = () => options.now?.() ?? Date.now();
2148
+ return {
2149
+ async run(runOptions = {}) {
2150
+ const report = { claimed: 0, sent: 0, filtered: 0, skipped: 0, retrying: 0, deferred: 0, failed: 0, stale: 0, unrecorded: 0 };
2151
+ const limit = runOptions.limit ?? options.batchSize ?? DEFAULT_BATCH_SIZE;
2152
+ const claimed = await options.claim({ limit, filter: runOptions.filter });
2153
+ report.claimed = claimed.length;
2154
+ const groups = /* @__PURE__ */ new Map();
2155
+ claimed.forEach((job, i) => {
2156
+ const key = options.group ? options.group(job) : `\0${i}`;
2157
+ const list = groups.get(key) ?? [];
2158
+ list.push(job);
2159
+ groups.set(key, list);
2160
+ });
2161
+ await Promise.all(
2162
+ [...groups.values()].map(async (group) => {
2163
+ let results;
2164
+ try {
2165
+ results = await options.send(group, { signal: runOptions.signal });
2166
+ if (results.length !== group.length) throw new Error("send answered the wrong number of results");
2167
+ } catch (error) {
2168
+ const failure = failureOf(error);
2169
+ results = group.map(() => failure);
2170
+ }
2171
+ const at = now();
2172
+ await Promise.all(
2173
+ group.map(async (job, i) => {
2174
+ const result = results[i];
2175
+ const outcome = result === "filtered" ? { status: "filtered" } : jobOutcome(result, job.attempt, {
2176
+ now: at,
2177
+ maxAttempts: options.maxAttempts,
2178
+ backoff: options.backoff,
2179
+ deferrals: job.deferrals,
2180
+ maxDeferrals: options.maxDeferrals
2181
+ });
2182
+ if (outcome.status === "failed" || outcome.status === "retry") {
2183
+ log(`${label(job)} ${outcome.status === "failed" ? "failed" : "will be retried"}: ${outcome.error}`);
2184
+ } else if (outcome.status === "skipped") {
2185
+ log(`${label(job)} skipped: ${outcome.reason}`);
2186
+ }
2187
+ let recorded;
2188
+ try {
2189
+ recorded = await options.record(job, outcome);
2190
+ } catch {
2191
+ report.unrecorded++;
2192
+ log(`Recording ${recordLabel(job)} failed`);
2193
+ return;
2194
+ }
2195
+ if (recorded === false) report.stale++;
2196
+ else if (outcome.status === "sent") report.sent++;
2197
+ else if (outcome.status === "filtered") report.filtered++;
2198
+ else if (outcome.status === "skipped") report.skipped++;
2199
+ else if (outcome.status === "failed") report.failed++;
2200
+ else if (outcome.counted) report.retrying++;
2201
+ else report.deferred++;
2202
+ })
2203
+ );
2204
+ })
2205
+ );
2206
+ return report;
2207
+ }
2208
+ };
2209
+ }
2210
+
2211
+ // src/destinations/runner.ts
2212
+ var DEFAULT_MAX_ATTEMPTS2 = DEFAULT_MAX_ATTEMPTS;
2213
+ var DEFAULT_MAX_DEFERRALS2 = DEFAULT_MAX_DEFERRALS;
2214
+ var DEFAULT_BATCH_SIZE2 = DEFAULT_BATCH_SIZE;
2215
+ var defaultBackoff2 = defaultBackoff;
2216
+ function deliveryOutcome(result, attempt, options = {}) {
2217
+ return jobOutcome(result.ok ? { ok: true, notSent: result.notSent } : result, attempt, options);
2218
+ }
2219
+ function createDeliveryRunner(options) {
2220
+ const log = (message) => {
2221
+ try {
2222
+ options.log?.(message);
2223
+ } catch {
2224
+ }
2225
+ };
2128
2226
  async function send(group, signal) {
2129
2227
  const first = group[0];
2130
2228
  const destination = options.destinations[first.kind];
@@ -2185,74 +2283,30 @@ function createDeliveryRunner(options) {
2185
2283
  let next = 0;
2186
2284
  return results.map((r) => r ?? sent[next++]);
2187
2285
  }
2188
- return {
2189
- async run(runOptions = {}) {
2190
- const report = {
2191
- claimed: 0,
2192
- sent: 0,
2193
- filtered: 0,
2194
- skipped: 0,
2195
- retrying: 0,
2196
- deferred: 0,
2197
- failed: 0,
2198
- stale: 0,
2199
- unrecorded: 0
2200
- };
2201
- const limit = runOptions.limit ?? options.batchSize ?? DEFAULT_BATCH_SIZE;
2202
- const claimed = await options.claim({ limit, filter: runOptions.filter });
2203
- report.claimed = claimed.length;
2204
- const groups = /* @__PURE__ */ new Map();
2205
- for (const delivery of claimed) {
2206
- const list = groups.get(delivery.destinationId) ?? [];
2207
- list.push(delivery);
2208
- groups.set(delivery.destinationId, list);
2209
- }
2210
- await Promise.all(
2211
- [...groups.values()].map(async (group) => {
2212
- const results = await send(group, runOptions.signal);
2213
- const at = now();
2214
- await Promise.all(
2215
- group.map(async (delivery, i) => {
2216
- const result = results[i];
2217
- const outcome = result === "filtered" ? { status: "filtered" } : deliveryOutcome(result, delivery.attempt, {
2218
- now: at,
2219
- maxAttempts: options.maxAttempts,
2220
- backoff: options.backoff,
2221
- deferrals: delivery.deferrals,
2222
- maxDeferrals: options.maxDeferrals
2223
- });
2224
- if (outcome.status === "sent" && result !== "filtered" && result.ok && result.skipped?.includes(delivery.submission.id)) {
2225
- outcome.alreadyThere = true;
2226
- }
2227
- if (outcome.status === "failed" || outcome.status === "retry") {
2228
- log(`Delivery ${delivery.id} (${delivery.kind}) ${outcome.status === "failed" ? "failed" : "will be retried"}: ${outcome.error}`);
2229
- } else if (outcome.status === "skipped") {
2230
- log(`Delivery ${delivery.id} (${delivery.kind}) skipped: ${outcome.reason}`);
2231
- }
2232
- let recorded;
2233
- try {
2234
- recorded = await options.record(delivery, outcome);
2235
- } catch {
2236
- report.unrecorded++;
2237
- log(`Recording delivery ${delivery.id} failed`);
2238
- return;
2239
- }
2240
- if (recorded === false) report.stale++;
2241
- else if (outcome.status === "sent") report.sent++;
2242
- else if (outcome.status === "filtered") report.filtered++;
2243
- else if (outcome.status === "skipped") report.skipped++;
2244
- else if (outcome.status === "failed") report.failed++;
2245
- else if (outcome.counted) report.retrying++;
2246
- else report.deferred++;
2247
- })
2248
- );
2249
- })
2250
- );
2251
- return report;
2252
- }
2253
- };
2286
+ return createRunner({
2287
+ claim: options.claim,
2288
+ // One group per destination: a sheet gets one append for all its rows.
2289
+ group: (delivery) => delivery.destinationId,
2290
+ async send(group, { signal }) {
2291
+ const results = await send(group, signal);
2292
+ return results.map((result, i) => {
2293
+ if (result === "filtered") return result;
2294
+ if (!result.ok) return result;
2295
+ return { ok: true, notSent: result.notSent, alreadyThere: result.skipped?.includes(group[i].submission.id) && !result.notSent };
2296
+ });
2297
+ },
2298
+ record: options.record,
2299
+ now: options.now,
2300
+ log: options.log,
2301
+ label: (delivery) => `Delivery ${delivery.id} (${delivery.kind})`,
2302
+ recordLabel: (delivery) => `delivery ${delivery.id}`,
2303
+ batchSize: options.batchSize,
2304
+ maxAttempts: options.maxAttempts,
2305
+ maxDeferrals: options.maxDeferrals,
2306
+ backoff: options.backoff
2307
+ });
2254
2308
  }
2255
2309
 
2256
- export { DEDUPE_WINDOW, DEFAULT_BATCH_SIZE, DEFAULT_MAX_ATTEMPTS, DEFAULT_MAX_DEFERRALS, DEFAULT_SHEET_NAME, DRIVE_FILE_SCOPE, DeliveryError, GOOGLE_TOKEN_URL, HEADER_SCAN_ROWS, HookUrlError, MAX_CONDITION_DEPTH, MAX_HOOK_URL_LENGTH, MAX_RECIPIENTS, MIN_WEBHOOK_SECRET_BYTES, REQUEST_TIMEOUT_MS, RESEND_API, SENSITIVE_MASK, SHEETS_API, SHEETS_SCOPE, SLACK_HOSTS, SLACK_VALUE_LIMIT, answerCell, checkHookHost, checkWebhookUrl, clearGoogleOAuthTokenCache, clearGoogleTokenCache, columnLetter, conditionOf, createDeliveryRunner, defaultBackoff, deliveryFailure, deliveryOutcome, deriveSecretKey, emailBody, emailDestination, escapeFormula, escapeHtml, escapeSlack, googleAccessToken, googleOAuthAccessToken, headerSafe, hookHost, httpError, idempotencyKey, isCondition, isPrivateHost, isPublicAddress, isRetryableStatus, isSafeAddress, isSafeFrom, mergeHeader, newWebhookSecret, openSecret, parseHookUrl, passesCondition, refreshGoogleAccessToken, renderEmail, resendProvider, responseRow, retryAfterSeconds, rowFor, sealSecret, secretKeyBytes, sheetsDestination, signJwt, signWebhook, slackDestination, slackMessage, verifyWebhook, visibleInputs, webhookDestination, webhookPayload };
2310
+ export { DEDUPE_WINDOW, DEFAULT_BATCH_SIZE2 as DEFAULT_BATCH_SIZE, DEFAULT_MAX_ATTEMPTS2 as DEFAULT_MAX_ATTEMPTS, DEFAULT_MAX_DEFERRALS2 as DEFAULT_MAX_DEFERRALS, DEFAULT_SHEET_NAME, DRIVE_FILE_SCOPE, DeliveryError, GOOGLE_TOKEN_URL, HEADER_SCAN_ROWS, HookUrlError, MAX_CONDITION_DEPTH, MAX_HOOK_URL_LENGTH, MAX_RECIPIENTS, MIN_WEBHOOK_SECRET_BYTES, REQUEST_TIMEOUT_MS, RESEND_API, SENSITIVE_MASK, SHEETS_API, SHEETS_SCOPE, SLACK_HOSTS, SLACK_VALUE_LIMIT, answerCell, checkHookHost, checkWebhookUrl, clearGoogleOAuthTokenCache, clearGoogleTokenCache, columnLetter, conditionOf, createDeliveryRunner, defaultBackoff2 as defaultBackoff, deliveryFailure, deliveryOutcome, deriveSecretKey, emailBody, emailDestination, escapeFormula, escapeHtml, escapeSlack, googleAccessToken, googleOAuthAccessToken, headerSafe, hookHost, httpError, idempotencyKey, isCondition, isPrivateHost, isPublicAddress, isRetryableStatus, isSafeAddress, isSafeFrom, mergeHeader, newWebhookSecret, openSecret, parseHookUrl, passesCondition, refreshGoogleAccessToken, renderEmail, resendProvider, responseRow, retryAfterSeconds, rowFor, sealSecret, secretKeyBytes, sheetsDestination, signJwt, signWebhook, slackDestination, slackMessage, verifyWebhook, visibleInputs, webhookDestination, webhookPayload };
2257
2311
  //# sourceMappingURL=destinations.js.map
2258
2312
  //# sourceMappingURL=destinations.js.map