@vxil/feature-configs 0.8.0 → 0.9.1
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/dist/index.d.ts +7 -4
- package/dist/index.js +47 -23
- package/package.json +1 -1
- package/src/index.ts +46 -23
package/dist/index.d.ts
CHANGED
|
@@ -6,9 +6,12 @@ export * from './canonicalJson.js';
|
|
|
6
6
|
export * from './publicAssets.js';
|
|
7
7
|
export declare const RESERVED_CREDIT_TYPES: ReadonlySet<string>;
|
|
8
8
|
/** A function binding's `retry.maxAttempts` ceiling, and the
|
|
9
|
-
* binding kinds that may carry `retry` — the platform
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* binding kinds that may carry `retry` — every lane the platform delivers
|
|
10
|
+
* through jobs. Since 2026-10-03 (JT-9) that includes `cron` (a failed tick is
|
|
11
|
+
* re-delivered on the ladder; pair it with `overlap: 'skip'` so a retrying
|
|
12
|
+
* tick never runs beside the next one) and `http` (the ASYNC lane only — a
|
|
13
|
+
* synchronous invoke always hands its caller the function's own status).
|
|
14
|
+
* Read by the schema, the deploy clamp and the CLI. */
|
|
12
15
|
export declare const FN_RETRY_MAX_ATTEMPTS = 5;
|
|
13
16
|
export declare const FN_RETRY_BINDING_KINDS: ReadonlySet<string>;
|
|
14
17
|
/** True when a credit_type is a vxil-COGS reserved type (grant/consume/config
|
|
@@ -359,7 +362,7 @@ export declare const FilesConfigSchema: import("@sinclair/typebox").TObject<{
|
|
|
359
362
|
downloadUrlTtl: import("@sinclair/typebox").TInteger;
|
|
360
363
|
quotas: import("@sinclair/typebox").TObject<{
|
|
361
364
|
maxObjectBytes: import("@sinclair/typebox").TInteger;
|
|
362
|
-
maxTotalBytes: import("@sinclair/typebox").TInteger
|
|
365
|
+
maxTotalBytes: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
|
|
363
366
|
maxObjectCount: import("@sinclair/typebox").TInteger;
|
|
364
367
|
}>;
|
|
365
368
|
allowedContentTypes: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
|
package/dist/index.js
CHANGED
|
@@ -42,11 +42,14 @@ if (!FormatRegistry.Has('email')) {
|
|
|
42
42
|
// config-write refusal share ONE list. payments-v1/core.ts re-exports these.
|
|
43
43
|
export const RESERVED_CREDIT_TYPES = new Set(['fn_cpu_ms']);
|
|
44
44
|
/** A function binding's `retry.maxAttempts` ceiling, and the
|
|
45
|
-
* binding kinds that may carry `retry` — the platform
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
* binding kinds that may carry `retry` — every lane the platform delivers
|
|
46
|
+
* through jobs. Since 2026-10-03 (JT-9) that includes `cron` (a failed tick is
|
|
47
|
+
* re-delivered on the ladder; pair it with `overlap: 'skip'` so a retrying
|
|
48
|
+
* tick never runs beside the next one) and `http` (the ASYNC lane only — a
|
|
49
|
+
* synchronous invoke always hands its caller the function's own status).
|
|
50
|
+
* Read by the schema, the deploy clamp and the CLI. */
|
|
48
51
|
export const FN_RETRY_MAX_ATTEMPTS = 5;
|
|
49
|
-
export const FN_RETRY_BINDING_KINDS = new Set(['queue', 'webhook', 'cmsHook', 'authHook']);
|
|
52
|
+
export const FN_RETRY_BINDING_KINDS = new Set(['queue', 'webhook', 'cmsHook', 'authHook', 'cron', 'http']);
|
|
50
53
|
/** True when a credit_type is a vxil-COGS reserved type (grant/consume/config
|
|
51
54
|
* of which is restricted to internal platform machinery). */
|
|
52
55
|
export function isReservedCreditType(creditType) {
|
|
@@ -341,7 +344,24 @@ export const JobsConfigSchema = Type.Object({
|
|
|
341
344
|
* pathological target (e.g. an always-throwing function) can generate. */
|
|
342
345
|
dlqDailyQuota: Type.Integer({ default: 0, minimum: 0, maximum: 100_000 }),
|
|
343
346
|
schedules: Type.Object({ maxPerTenant: Type.Integer({ default: 50, minimum: 1, maximum: 1000 }) }, { default: {} }),
|
|
344
|
-
concurrency: Type.Object(
|
|
347
|
+
concurrency: Type.Object(
|
|
348
|
+
/** Per-tenant cap on PLAIN runs (queue / cron / event deliveries,
|
|
349
|
+
* incl. platform-delivered function triggers) in flight at once; generation
|
|
350
|
+
* runs have their own `generation.maxConcurrent`. EFFECTIVE value =
|
|
351
|
+
* min(this, the tenant's fair share of the shared delivery consumer): the
|
|
352
|
+
* platform runs at most 75 queue-lane deliveries in flight for ALL tenants
|
|
353
|
+
* together, and one tenant may hold at most 25 of them (jobs-v1 core.ts
|
|
354
|
+
* TENANT_FAIR_SHARE). Values 26..100 still validate (existing configs keep
|
|
355
|
+
* working) but buy nothing past the share. Over-cap runs wait (deferred,
|
|
356
|
+
* never rejected). The schema `description` below is the tenant-facing
|
|
357
|
+
* copy of this (planner catalog, generated config schema) — keep both in
|
|
358
|
+
* step. */
|
|
359
|
+
{
|
|
360
|
+
maxConcurrent: Type.Integer({
|
|
361
|
+
default: 10, minimum: 1, maximum: 100,
|
|
362
|
+
description: 'Plain runs (queued jobs, cron fires, event deliveries, platform-delivered function triggers) delivered at once; generation runs have their own generation.maxConcurrent. Effective at most 25 (the per-project share of the shared delivery capacity): 26..100 validate but buy nothing more. Over-cap runs wait, never rejected.',
|
|
363
|
+
}),
|
|
364
|
+
}, { default: {} }),
|
|
345
365
|
// Generation lifecycle knobs (guide ch. 6, jobs). The defaults and bounds are
|
|
346
366
|
// declared ONCE, as GENERATION_DEFAULTS / GENERATION_BOUNDS below this schema
|
|
347
367
|
// (2026-09-25): jobs-v1's resolveGenerationConfig imports them and clamps a
|
|
@@ -680,11 +700,16 @@ export const FilesConfigSchema = Type.Object({
|
|
|
680
700
|
quotas: Type.Object({
|
|
681
701
|
// maximum caps are a defense-in-depth ceiling on tenant-editable storage —
|
|
682
702
|
// object storage is cheap but the database-resident metadata + abuse aren't
|
|
683
|
-
// (pricing review 2026-07-10). 5 GiB/object, 1 TiB/tenant absolute
|
|
684
|
-
// (Free/Dev 10 GB · Team 50 GB · Business 256 GB) are a follow-up needing the
|
|
685
|
-
// tenant tier threaded to files-v1 (plan tiers).
|
|
703
|
+
// (pricing review 2026-07-10). 5 GiB/object, 1 TiB/tenant absolute.
|
|
686
704
|
maxObjectBytes: Type.Integer({ default: 100 * 1024 * 1024, minimum: 1, maximum: 5 * 1024 * 1024 * 1024 }),
|
|
687
|
-
|
|
705
|
+
// NO default (2026-10-04): UNSET means "the plan's storage ceiling"
|
|
706
|
+
// (control-plane plans.ts FILES_MAX_TOTAL_BYTES_BY_TIER — Free/Developer
|
|
707
|
+
// 10 GiB, Team 50 GiB, Business 256 GiB, Enterprise 1 TiB), resolved by
|
|
708
|
+
// files-v1 where the quota is enforced, so a plan change applies at once.
|
|
709
|
+
// A value is the project's own LOWER cap (above the plan → 422 at config
|
|
710
|
+
// write; a downgrade clamps it). It used to default to 10 GiB on every
|
|
711
|
+
// plan, so a Team project that never set it stayed at 10 GiB.
|
|
712
|
+
maxTotalBytes: Type.Optional(Type.Integer({ minimum: 1, maximum: 1024 * 1024 * 1024 * 1024 })),
|
|
688
713
|
maxObjectCount: Type.Integer({ default: 100000, minimum: 1, maximum: 100_000_000 }),
|
|
689
714
|
}, { default: {} }),
|
|
690
715
|
allowedContentTypes: Type.Array(Type.String(), { default: ['*'], maxItems: 100 }),
|
|
@@ -706,7 +731,7 @@ export const FilesConfigSchema = Type.Object({
|
|
|
706
731
|
enabled: Type.Boolean({ default: false }),
|
|
707
732
|
// per-bucket default; omitted = never expire by default
|
|
708
733
|
defaultExpiresInSeconds: Type.Optional(Type.Integer({ minimum: 60 })),
|
|
709
|
-
sweepCron: Type.String({ default: '
|
|
734
|
+
sweepCron: Type.String({ default: '*/15 * * * *' }), // jobs-v1 TTL sweep schedule (every 15 min since 2026-10-04; was hourly)
|
|
710
735
|
})),
|
|
711
736
|
extractText: Type.Optional(Type.Object({
|
|
712
737
|
enabled: Type.Boolean({ default: false }),
|
|
@@ -1413,8 +1438,8 @@ export const FunctionsConfigSchema = Type.Object({
|
|
|
1413
1438
|
collection: Type.Optional(Type.String()), // cmsHook: the CMS collection slug
|
|
1414
1439
|
event: Type.Optional(Type.String()), // cmsHook: beforeCreate|beforeUpdate|beforeWrite · authHook: 'user.created'
|
|
1415
1440
|
// (2026-09-25) the per-binding opt-in to re-delivery on
|
|
1416
|
-
// queue / webhook / cmsHook / authHook
|
|
1417
|
-
//
|
|
1441
|
+
// queue / webhook / cmsHook / authHook, and since 2026-10-03 on
|
|
1442
|
+
// cron and http (the async lane only). Absent = the ACK-200 default. The
|
|
1418
1443
|
// receiver answers a failed attempt as an enveloped 503 (ladder)
|
|
1419
1444
|
// and the last one as a terminal 409 (dead + job.dead_lettered);
|
|
1420
1445
|
// effective attempts = min(maxAttempts, jobs.retry.defaultMaxAttempts).
|
|
@@ -2127,9 +2152,8 @@ export function validateFeatureConfig(feature, raw) {
|
|
|
2127
2152
|
if (b.overlap !== undefined && b.kind !== 'cron') {
|
|
2128
2153
|
errs.push(`/functions/${name}/bindings/${i}: 'overlap' applies to cron bindings only (not '${b.kind}')`);
|
|
2129
2154
|
}
|
|
2130
|
-
// retry is an opt-in for the platform-delivered
|
|
2131
|
-
//
|
|
2132
|
-
// tick's retry would overlap the next tick.
|
|
2155
|
+
// retry is an opt-in for the platform-delivered lanes
|
|
2156
|
+
// (FN_RETRY_BINDING_KINDS — every binding kind since JT-9)
|
|
2133
2157
|
if (b.retry !== undefined && !FN_RETRY_BINDING_KINDS.has(b.kind)) {
|
|
2134
2158
|
errs.push(`/functions/${name}/bindings/${i}: 'retry' applies to ${[...FN_RETRY_BINDING_KINDS].join(' | ')} bindings only (not '${b.kind}')`);
|
|
2135
2159
|
}
|
|
@@ -2223,15 +2247,15 @@ export const FEATURE_KEYS = [
|
|
|
2223
2247
|
];
|
|
2224
2248
|
export const FEATURE_HINTS = {
|
|
2225
2249
|
notifications: {
|
|
2226
|
-
whenToUse: 'Sending email (transactional or broadcast) or in-app inbox messages to end-users: welcomes, receipts, reminders, alerts, digests.',
|
|
2250
|
+
whenToUse: 'Sending email (transactional or broadcast) or in-app inbox messages to end-users: welcomes, receipts, reminders, alerts, digests. A send can be scheduled for a known time (send_at), so a one-off reminder needs no job or function.',
|
|
2227
2251
|
signals: ['email', 'notify', 'alert', 'reminder', 'welcome', 'receipt', 'digest', 'inbox'],
|
|
2228
2252
|
notFor: 'Live in-page updates (realtime) or activity timelines (activity-feed).',
|
|
2229
2253
|
dependsOn: [],
|
|
2230
2254
|
},
|
|
2231
2255
|
jobs: {
|
|
2232
|
-
whenToUse: 'Background work: scheduled/cron tasks, delayed
|
|
2233
|
-
signals: ['schedule', 'cron', 'daily', 'background', 'queue', 'later', 'recurring', 'batch'],
|
|
2234
|
-
notFor: 'Simple request-response logic that completes inline.',
|
|
2256
|
+
whenToUse: 'Background work: scheduled/cron tasks, delayed runs, retries, queues, runs that wait for an event, per-user serialisation (concurrency keys), debounced bursts, start deadlines, fan-in (a batch completion event), per-user schedules in their own time zone. Also the bookkeeping for LONG work that runs on the tenant\'s own runtime (renders, transcodes, model calls): a generation run holds reserved credits, a deadline, a signed completion callback and live progress mirrored onto a cms row.',
|
|
2257
|
+
signals: ['schedule', 'cron', 'daily', 'background', 'queue', 'later', 'recurring', 'batch', 'render', 'transcode', 'progress', 'long-running'],
|
|
2258
|
+
notFor: 'Simple request-response logic that completes inline; it is a queue with controls, not a workflow engine, and it never runs the heavy compute itself.',
|
|
2235
2259
|
dependsOn: [],
|
|
2236
2260
|
},
|
|
2237
2261
|
auth: {
|
|
@@ -2247,8 +2271,8 @@ export const FEATURE_HINTS = {
|
|
|
2247
2271
|
dependsOn: [],
|
|
2248
2272
|
},
|
|
2249
2273
|
files: {
|
|
2250
|
-
whenToUse: 'End-users upload or download files/images/documents: avatars, attachments, photos, PDFs, media libraries.',
|
|
2251
|
-
signals: ['upload', 'image', 'photo', 'file', 'attachment', 'document', 'avatar', 'media'],
|
|
2274
|
+
whenToUse: 'End-users upload or download files/images/documents: avatars, attachments, photos, PDFs, media libraries. Media shown publicly (galleries, product photos, video, audio, fonts) is published to a public CDN URL, with named image variant presets such as thumbnails (publicAssets).',
|
|
2275
|
+
signals: ['upload', 'image', 'photo', 'file', 'attachment', 'document', 'avatar', 'media', 'gallery', 'thumbnail', 'video', 'CDN'],
|
|
2252
2276
|
notFor: 'Structured records (cms) or text content authored in-app.',
|
|
2253
2277
|
dependsOn: [],
|
|
2254
2278
|
},
|
|
@@ -2265,7 +2289,7 @@ export const FEATURE_HINTS = {
|
|
|
2265
2289
|
dependsOn: [],
|
|
2266
2290
|
},
|
|
2267
2291
|
cms: {
|
|
2268
|
-
whenToUse: 'The data spine: typed record collections with fields, relations, owner-scoping, draft/publish, and optional keyless public reads. Most apps are, underneath, cms collections.',
|
|
2292
|
+
whenToUse: 'The data spine: typed record collections with fields, relations, owner-scoping, draft/publish, indexed filters, and optional keyless public reads. Most apps are, underneath, cms collections.',
|
|
2269
2293
|
signals: ['store', 'list', 'catalog', 'posts', 'products', 'records', 'items', 'content', 'blog', 'tasks', 'listings'],
|
|
2270
2294
|
notFor: 'End-user identity (auth) or file bytes (files).',
|
|
2271
2295
|
dependsOn: [],
|
|
@@ -2327,7 +2351,7 @@ export const FEATURE_HINTS = {
|
|
|
2327
2351
|
functions: {
|
|
2328
2352
|
whenToUse: 'ONLY for truly-unique server logic no feature or cms rule can express: bespoke sagas, custom integrations over the egress guard, computed endpoints. Prefer features/cms first; functions are the escape hatch.',
|
|
2329
2353
|
signals: ['custom logic', 'custom backend', 'integration with', 'workflow', 'saga'],
|
|
2330
|
-
notFor: 'Anything a shipped feature or a cms lifecycle hook already covers.',
|
|
2354
|
+
notFor: 'Anything a shipped feature or a cms lifecycle hook already covers, and heavy or long compute (renders, ffmpeg, ML) — that runs on the tenant\'s own runtime, tracked by a jobs generation run.',
|
|
2331
2355
|
dependsOn: [],
|
|
2332
2356
|
},
|
|
2333
2357
|
copilot: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vxil/feature-configs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "The per-feature configuration schemas and validators behind vxil.config.ts (published for @vxil/cli and @vxil/config).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://vxil.com",
|
package/src/index.ts
CHANGED
|
@@ -46,11 +46,14 @@ if (!FormatRegistry.Has('email')) {
|
|
|
46
46
|
export const RESERVED_CREDIT_TYPES: ReadonlySet<string> = new Set<string>(['fn_cpu_ms']);
|
|
47
47
|
|
|
48
48
|
/** A function binding's `retry.maxAttempts` ceiling, and the
|
|
49
|
-
* binding kinds that may carry `retry` — the platform
|
|
50
|
-
*
|
|
51
|
-
*
|
|
49
|
+
* binding kinds that may carry `retry` — every lane the platform delivers
|
|
50
|
+
* through jobs. Since 2026-10-03 (JT-9) that includes `cron` (a failed tick is
|
|
51
|
+
* re-delivered on the ladder; pair it with `overlap: 'skip'` so a retrying
|
|
52
|
+
* tick never runs beside the next one) and `http` (the ASYNC lane only — a
|
|
53
|
+
* synchronous invoke always hands its caller the function's own status).
|
|
54
|
+
* Read by the schema, the deploy clamp and the CLI. */
|
|
52
55
|
export const FN_RETRY_MAX_ATTEMPTS = 5;
|
|
53
|
-
export const FN_RETRY_BINDING_KINDS: ReadonlySet<string> = new Set(['queue', 'webhook', 'cmsHook', 'authHook']);
|
|
56
|
+
export const FN_RETRY_BINDING_KINDS: ReadonlySet<string> = new Set(['queue', 'webhook', 'cmsHook', 'authHook', 'cron', 'http']);
|
|
54
57
|
|
|
55
58
|
/** True when a credit_type is a vxil-COGS reserved type (grant/consume/config
|
|
56
59
|
* of which is restricted to internal platform machinery). */
|
|
@@ -399,7 +402,23 @@ export const JobsConfigSchema = Type.Object({
|
|
|
399
402
|
{ default: {} },
|
|
400
403
|
),
|
|
401
404
|
concurrency: Type.Object(
|
|
402
|
-
|
|
405
|
+
/** Per-tenant cap on PLAIN runs (queue / cron / event deliveries,
|
|
406
|
+
* incl. platform-delivered function triggers) in flight at once; generation
|
|
407
|
+
* runs have their own `generation.maxConcurrent`. EFFECTIVE value =
|
|
408
|
+
* min(this, the tenant's fair share of the shared delivery consumer): the
|
|
409
|
+
* platform runs at most 75 queue-lane deliveries in flight for ALL tenants
|
|
410
|
+
* together, and one tenant may hold at most 25 of them (jobs-v1 core.ts
|
|
411
|
+
* TENANT_FAIR_SHARE). Values 26..100 still validate (existing configs keep
|
|
412
|
+
* working) but buy nothing past the share. Over-cap runs wait (deferred,
|
|
413
|
+
* never rejected). The schema `description` below is the tenant-facing
|
|
414
|
+
* copy of this (planner catalog, generated config schema) — keep both in
|
|
415
|
+
* step. */
|
|
416
|
+
{
|
|
417
|
+
maxConcurrent: Type.Integer({
|
|
418
|
+
default: 10, minimum: 1, maximum: 100,
|
|
419
|
+
description: 'Plain runs (queued jobs, cron fires, event deliveries, platform-delivered function triggers) delivered at once; generation runs have their own generation.maxConcurrent. Effective at most 25 (the per-project share of the shared delivery capacity): 26..100 validate but buy nothing more. Over-cap runs wait, never rejected.',
|
|
420
|
+
}),
|
|
421
|
+
},
|
|
403
422
|
{ default: {} },
|
|
404
423
|
),
|
|
405
424
|
// Generation lifecycle knobs (guide ch. 6, jobs). The defaults and bounds are
|
|
@@ -804,11 +823,16 @@ export const FilesConfigSchema = Type.Object({
|
|
|
804
823
|
{
|
|
805
824
|
// maximum caps are a defense-in-depth ceiling on tenant-editable storage —
|
|
806
825
|
// object storage is cheap but the database-resident metadata + abuse aren't
|
|
807
|
-
// (pricing review 2026-07-10). 5 GiB/object, 1 TiB/tenant absolute
|
|
808
|
-
// (Free/Dev 10 GB · Team 50 GB · Business 256 GB) are a follow-up needing the
|
|
809
|
-
// tenant tier threaded to files-v1 (plan tiers).
|
|
826
|
+
// (pricing review 2026-07-10). 5 GiB/object, 1 TiB/tenant absolute.
|
|
810
827
|
maxObjectBytes: Type.Integer({ default: 100 * 1024 * 1024, minimum: 1, maximum: 5 * 1024 * 1024 * 1024 }),
|
|
811
|
-
|
|
828
|
+
// NO default (2026-10-04): UNSET means "the plan's storage ceiling"
|
|
829
|
+
// (control-plane plans.ts FILES_MAX_TOTAL_BYTES_BY_TIER — Free/Developer
|
|
830
|
+
// 10 GiB, Team 50 GiB, Business 256 GiB, Enterprise 1 TiB), resolved by
|
|
831
|
+
// files-v1 where the quota is enforced, so a plan change applies at once.
|
|
832
|
+
// A value is the project's own LOWER cap (above the plan → 422 at config
|
|
833
|
+
// write; a downgrade clamps it). It used to default to 10 GiB on every
|
|
834
|
+
// plan, so a Team project that never set it stayed at 10 GiB.
|
|
835
|
+
maxTotalBytes: Type.Optional(Type.Integer({ minimum: 1, maximum: 1024 * 1024 * 1024 * 1024 })),
|
|
812
836
|
maxObjectCount: Type.Integer({ default: 100000, minimum: 1, maximum: 100_000_000 }),
|
|
813
837
|
},
|
|
814
838
|
{ default: {} },
|
|
@@ -835,7 +859,7 @@ export const FilesConfigSchema = Type.Object({
|
|
|
835
859
|
enabled: Type.Boolean({ default: false }),
|
|
836
860
|
// per-bucket default; omitted = never expire by default
|
|
837
861
|
defaultExpiresInSeconds: Type.Optional(Type.Integer({ minimum: 60 })),
|
|
838
|
-
sweepCron: Type.String({ default: '
|
|
862
|
+
sweepCron: Type.String({ default: '*/15 * * * *' }), // jobs-v1 TTL sweep schedule (every 15 min since 2026-10-04; was hourly)
|
|
839
863
|
})),
|
|
840
864
|
extractText: Type.Optional(Type.Object({
|
|
841
865
|
enabled: Type.Boolean({ default: false }),
|
|
@@ -1754,8 +1778,8 @@ export const FunctionsConfigSchema = Type.Object({
|
|
|
1754
1778
|
collection: Type.Optional(Type.String()), // cmsHook: the CMS collection slug
|
|
1755
1779
|
event: Type.Optional(Type.String()), // cmsHook: beforeCreate|beforeUpdate|beforeWrite · authHook: 'user.created'
|
|
1756
1780
|
// (2026-09-25) the per-binding opt-in to re-delivery on
|
|
1757
|
-
// queue / webhook / cmsHook / authHook
|
|
1758
|
-
//
|
|
1781
|
+
// queue / webhook / cmsHook / authHook, and since 2026-10-03 on
|
|
1782
|
+
// cron and http (the async lane only). Absent = the ACK-200 default. The
|
|
1759
1783
|
// receiver answers a failed attempt as an enveloped 503 (ladder)
|
|
1760
1784
|
// and the last one as a terminal 409 (dead + job.dead_lettered);
|
|
1761
1785
|
// effective attempts = min(maxAttempts, jobs.retry.defaultMaxAttempts).
|
|
@@ -2567,9 +2591,8 @@ export function validateFeatureConfig(feature: string, raw: unknown): ConfigVali
|
|
|
2567
2591
|
if (b.overlap !== undefined && b.kind !== 'cron') {
|
|
2568
2592
|
errs.push(`/functions/${name}/bindings/${i}: 'overlap' applies to cron bindings only (not '${b.kind}')`);
|
|
2569
2593
|
}
|
|
2570
|
-
// retry is an opt-in for the platform-delivered
|
|
2571
|
-
//
|
|
2572
|
-
// tick's retry would overlap the next tick.
|
|
2594
|
+
// retry is an opt-in for the platform-delivered lanes
|
|
2595
|
+
// (FN_RETRY_BINDING_KINDS — every binding kind since JT-9)
|
|
2573
2596
|
if (b.retry !== undefined && !FN_RETRY_BINDING_KINDS.has(b.kind)) {
|
|
2574
2597
|
errs.push(`/functions/${name}/bindings/${i}: 'retry' applies to ${[...FN_RETRY_BINDING_KINDS].join(' | ')} bindings only (not '${b.kind}')`);
|
|
2575
2598
|
}
|
|
@@ -2683,15 +2706,15 @@ export interface PlannerHint {
|
|
|
2683
2706
|
|
|
2684
2707
|
export const FEATURE_HINTS: Record<FeatureKey, PlannerHint> = {
|
|
2685
2708
|
notifications: {
|
|
2686
|
-
whenToUse: 'Sending email (transactional or broadcast) or in-app inbox messages to end-users: welcomes, receipts, reminders, alerts, digests.',
|
|
2709
|
+
whenToUse: 'Sending email (transactional or broadcast) or in-app inbox messages to end-users: welcomes, receipts, reminders, alerts, digests. A send can be scheduled for a known time (send_at), so a one-off reminder needs no job or function.',
|
|
2687
2710
|
signals: ['email', 'notify', 'alert', 'reminder', 'welcome', 'receipt', 'digest', 'inbox'],
|
|
2688
2711
|
notFor: 'Live in-page updates (realtime) or activity timelines (activity-feed).',
|
|
2689
2712
|
dependsOn: [],
|
|
2690
2713
|
},
|
|
2691
2714
|
jobs: {
|
|
2692
|
-
whenToUse: 'Background work: scheduled/cron tasks, delayed
|
|
2693
|
-
signals: ['schedule', 'cron', 'daily', 'background', 'queue', 'later', 'recurring', 'batch'],
|
|
2694
|
-
notFor: 'Simple request-response logic that completes inline.',
|
|
2715
|
+
whenToUse: 'Background work: scheduled/cron tasks, delayed runs, retries, queues, runs that wait for an event, per-user serialisation (concurrency keys), debounced bursts, start deadlines, fan-in (a batch completion event), per-user schedules in their own time zone. Also the bookkeeping for LONG work that runs on the tenant\'s own runtime (renders, transcodes, model calls): a generation run holds reserved credits, a deadline, a signed completion callback and live progress mirrored onto a cms row.',
|
|
2716
|
+
signals: ['schedule', 'cron', 'daily', 'background', 'queue', 'later', 'recurring', 'batch', 'render', 'transcode', 'progress', 'long-running'],
|
|
2717
|
+
notFor: 'Simple request-response logic that completes inline; it is a queue with controls, not a workflow engine, and it never runs the heavy compute itself.',
|
|
2695
2718
|
dependsOn: [],
|
|
2696
2719
|
},
|
|
2697
2720
|
auth: {
|
|
@@ -2707,8 +2730,8 @@ export const FEATURE_HINTS: Record<FeatureKey, PlannerHint> = {
|
|
|
2707
2730
|
dependsOn: [],
|
|
2708
2731
|
},
|
|
2709
2732
|
files: {
|
|
2710
|
-
whenToUse: 'End-users upload or download files/images/documents: avatars, attachments, photos, PDFs, media libraries.',
|
|
2711
|
-
signals: ['upload', 'image', 'photo', 'file', 'attachment', 'document', 'avatar', 'media'],
|
|
2733
|
+
whenToUse: 'End-users upload or download files/images/documents: avatars, attachments, photos, PDFs, media libraries. Media shown publicly (galleries, product photos, video, audio, fonts) is published to a public CDN URL, with named image variant presets such as thumbnails (publicAssets).',
|
|
2734
|
+
signals: ['upload', 'image', 'photo', 'file', 'attachment', 'document', 'avatar', 'media', 'gallery', 'thumbnail', 'video', 'CDN'],
|
|
2712
2735
|
notFor: 'Structured records (cms) or text content authored in-app.',
|
|
2713
2736
|
dependsOn: [],
|
|
2714
2737
|
},
|
|
@@ -2725,7 +2748,7 @@ export const FEATURE_HINTS: Record<FeatureKey, PlannerHint> = {
|
|
|
2725
2748
|
dependsOn: [],
|
|
2726
2749
|
},
|
|
2727
2750
|
cms: {
|
|
2728
|
-
whenToUse: 'The data spine: typed record collections with fields, relations, owner-scoping, draft/publish, and optional keyless public reads. Most apps are, underneath, cms collections.',
|
|
2751
|
+
whenToUse: 'The data spine: typed record collections with fields, relations, owner-scoping, draft/publish, indexed filters, and optional keyless public reads. Most apps are, underneath, cms collections.',
|
|
2729
2752
|
signals: ['store', 'list', 'catalog', 'posts', 'products', 'records', 'items', 'content', 'blog', 'tasks', 'listings'],
|
|
2730
2753
|
notFor: 'End-user identity (auth) or file bytes (files).',
|
|
2731
2754
|
dependsOn: [],
|
|
@@ -2787,7 +2810,7 @@ export const FEATURE_HINTS: Record<FeatureKey, PlannerHint> = {
|
|
|
2787
2810
|
functions: {
|
|
2788
2811
|
whenToUse: 'ONLY for truly-unique server logic no feature or cms rule can express: bespoke sagas, custom integrations over the egress guard, computed endpoints. Prefer features/cms first; functions are the escape hatch.',
|
|
2789
2812
|
signals: ['custom logic', 'custom backend', 'integration with', 'workflow', 'saga'],
|
|
2790
|
-
notFor: 'Anything a shipped feature or a cms lifecycle hook already covers.',
|
|
2813
|
+
notFor: 'Anything a shipped feature or a cms lifecycle hook already covers, and heavy or long compute (renders, ffmpeg, ML) — that runs on the tenant\'s own runtime, tracked by a jobs generation run.',
|
|
2791
2814
|
dependsOn: [],
|
|
2792
2815
|
},
|
|
2793
2816
|
copilot: {
|