@helyx/bot 0.4.2 → 0.4.4
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/CHANGELOG.md +29 -0
- package/README.md +13 -0
- package/dist/cloud-connector.d.ts +7 -0
- package/dist/cloud-connector.js +118 -1
- package/dist/cloud-control-dispatcher.js +84 -0
- package/dist/control-server.d.ts +187 -5
- package/dist/control-server.js +673 -6
- package/dist/developer-operations-module.js +8 -0
- package/dist/discord-owned-message-service.d.ts +39 -0
- package/dist/discord-owned-message-service.js +138 -0
- package/dist/events.d.ts +70 -1
- package/dist/events.js +557 -5
- package/dist/index.js +61 -13
- package/dist/interactions.d.ts +3 -2
- package/dist/interactions.js +66 -31
- package/dist/module-logging.d.ts +1 -0
- package/dist/module-logging.js +19 -1
- package/dist/modules.d.ts +9 -0
- package/dist/modules.js +45 -0
- package/dist/resource-authorization.d.ts +7 -0
- package/dist/resource-authorization.js +12 -0
- package/dist/scheduled-task-runner.d.ts +130 -0
- package/dist/scheduled-task-runner.js +417 -0
- package/dist/shard-cloud-proxy.d.ts +4 -0
- package/dist/shard-cloud-proxy.js +34 -0
- package/dist/shard-contracts.d.ts +574 -23
- package/dist/shard-contracts.js +346 -0
- package/dist/shard-coordinator.d.ts +5 -3
- package/dist/shard-coordinator.js +85 -2
- package/dist/shard-router.d.ts +10 -0
- package/dist/shard-router.js +58 -0
- package/dist/shard-supervisor.d.ts +2 -1
- package/dist/shard-supervisor.js +187 -5
- package/dist/shard-worker.d.ts +9 -3
- package/dist/shard-worker.js +97 -16
- package/dist/support-module.d.ts +9 -0
- package/dist/support-module.js +172 -0
- package/package.json +9 -9
package/dist/shard-contracts.js
CHANGED
|
@@ -43,6 +43,36 @@ const configurationValue = z.record(z.string().min(1).max(100), z.union([
|
|
|
43
43
|
z.number().finite(),
|
|
44
44
|
z.array(z.string().max(4_000)).max(100),
|
|
45
45
|
]));
|
|
46
|
+
const resourceType = z.string().regex(/^[a-z][a-z0-9_-]*$/u);
|
|
47
|
+
const resourceId = z.string().trim().min(1).max(100);
|
|
48
|
+
const resourceCursor = z.string().min(1).max(2_048);
|
|
49
|
+
const managedResourceValue = z.custom((value) => isBoundedManagedResourceValue(value), "Managed resource value exceeds the transport limits");
|
|
50
|
+
const managedResourceObject = z
|
|
51
|
+
.record(z.string().min(1).max(100), managedResourceValue)
|
|
52
|
+
.refine(hasBoundedManagedResourceBytes);
|
|
53
|
+
const managedResourceFilters = z
|
|
54
|
+
.record(z.string().regex(/^[a-z][a-zA-Z0-9_]*$/u), z.string().trim().min(1).max(100))
|
|
55
|
+
.refine((value) => Object.keys(value).length <= 5);
|
|
56
|
+
const scheduledTaskPayload = z
|
|
57
|
+
.record(z.string().regex(/^[a-z][A-Za-z0-9]{0,62}Id$/u), z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u))
|
|
58
|
+
.refine((value) => Object.keys(value).length <= 20)
|
|
59
|
+
.refine((value) => Buffer.byteLength(JSON.stringify(value), "utf8") <= 2_048);
|
|
60
|
+
const scheduledTaskResult = z.discriminatedUnion("outcome", [
|
|
61
|
+
z.object({ outcome: z.literal("completed") }).strict(),
|
|
62
|
+
z
|
|
63
|
+
.object({
|
|
64
|
+
outcome: z.enum(["deferred", "retry"]),
|
|
65
|
+
delayMs: z.number().int().min(100).max(86_400_000),
|
|
66
|
+
safeCode: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,99}$/u),
|
|
67
|
+
})
|
|
68
|
+
.strict(),
|
|
69
|
+
z
|
|
70
|
+
.object({
|
|
71
|
+
outcome: z.literal("requires_review"),
|
|
72
|
+
safeCode: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,99}$/u),
|
|
73
|
+
})
|
|
74
|
+
.strict(),
|
|
75
|
+
]);
|
|
46
76
|
export const shardIdentitySchema = z
|
|
47
77
|
.object({
|
|
48
78
|
version: z.literal(SHARD_CONTRACT_VERSION),
|
|
@@ -108,6 +138,27 @@ const sessionStartBudgetSchema = z
|
|
|
108
138
|
observedAt: dateTime,
|
|
109
139
|
})
|
|
110
140
|
.strict();
|
|
141
|
+
const schedulerWorkHealthSchema = z
|
|
142
|
+
.object({
|
|
143
|
+
sampleLimit: z.number().int().min(1).max(5_000),
|
|
144
|
+
sampled: z.number().int().min(0),
|
|
145
|
+
ready: z.number().int().min(0),
|
|
146
|
+
expiredLeases: z.number().int().min(0),
|
|
147
|
+
retried: z.number().int().min(0),
|
|
148
|
+
requiresReview: z.number().int().min(0),
|
|
149
|
+
oldestReadyAt: dateTime.nullable(),
|
|
150
|
+
})
|
|
151
|
+
.strict();
|
|
152
|
+
export const schedulerOperationalHealthSchema = z.discriminatedUnion("status", [
|
|
153
|
+
z
|
|
154
|
+
.object({
|
|
155
|
+
status: z.literal("available"),
|
|
156
|
+
oneShot: schedulerWorkHealthSchema,
|
|
157
|
+
coalesced: schedulerWorkHealthSchema,
|
|
158
|
+
})
|
|
159
|
+
.strict(),
|
|
160
|
+
z.object({ status: z.literal("unavailable") }).strict(),
|
|
161
|
+
]);
|
|
111
162
|
export const shardHealthSnapshotSchema = z
|
|
112
163
|
.object({
|
|
113
164
|
version: z.literal(SHARD_CONTRACT_VERSION),
|
|
@@ -125,6 +176,7 @@ export const shardHealthSnapshotSchema = z
|
|
|
125
176
|
database: z.enum(["healthy", "degraded", "unavailable"]),
|
|
126
177
|
})
|
|
127
178
|
.strict(),
|
|
179
|
+
scheduler: schedulerOperationalHealthSchema.optional(),
|
|
128
180
|
sessionStarts: sessionStartBudgetSchema.optional(),
|
|
129
181
|
shards: z.array(shardProcessHealthSchema).max(4_096),
|
|
130
182
|
})
|
|
@@ -193,6 +245,118 @@ export const shardControlOperationSchema = z.discriminatedUnion("kind", [
|
|
|
193
245
|
...mutationContext.shape,
|
|
194
246
|
})
|
|
195
247
|
.strict(),
|
|
248
|
+
z
|
|
249
|
+
.object({
|
|
250
|
+
kind: z.literal("guild.module.resource.list"),
|
|
251
|
+
guildId: snowflake,
|
|
252
|
+
moduleId,
|
|
253
|
+
resourceType,
|
|
254
|
+
cursor: resourceCursor.optional(),
|
|
255
|
+
limit: z.number().int().min(1).max(50),
|
|
256
|
+
search: z.string().trim().min(1).max(200).optional(),
|
|
257
|
+
filters: managedResourceFilters.optional(),
|
|
258
|
+
...mutationContext.shape,
|
|
259
|
+
})
|
|
260
|
+
.strict(),
|
|
261
|
+
z
|
|
262
|
+
.object({
|
|
263
|
+
kind: z.literal("guild.module.resource.read"),
|
|
264
|
+
guildId: snowflake,
|
|
265
|
+
moduleId,
|
|
266
|
+
resourceType,
|
|
267
|
+
resourceId,
|
|
268
|
+
...mutationContext.shape,
|
|
269
|
+
})
|
|
270
|
+
.strict(),
|
|
271
|
+
z
|
|
272
|
+
.object({
|
|
273
|
+
kind: z.literal("guild.module.resource.relation.list"),
|
|
274
|
+
guildId: snowflake,
|
|
275
|
+
moduleId,
|
|
276
|
+
resourceType,
|
|
277
|
+
resourceId,
|
|
278
|
+
relationId: resourceType,
|
|
279
|
+
cursor: resourceCursor.optional(),
|
|
280
|
+
limit: z.number().int().min(1).max(50),
|
|
281
|
+
...mutationContext.shape,
|
|
282
|
+
})
|
|
283
|
+
.strict(),
|
|
284
|
+
z
|
|
285
|
+
.object({
|
|
286
|
+
kind: z.literal("guild.module.resource.preview"),
|
|
287
|
+
guildId: snowflake,
|
|
288
|
+
moduleId,
|
|
289
|
+
resourceType,
|
|
290
|
+
value: managedResourceObject,
|
|
291
|
+
...mutationContext.shape,
|
|
292
|
+
})
|
|
293
|
+
.strict(),
|
|
294
|
+
z
|
|
295
|
+
.object({
|
|
296
|
+
kind: z.literal("guild.module.resource.create"),
|
|
297
|
+
guildId: snowflake,
|
|
298
|
+
moduleId,
|
|
299
|
+
resourceType,
|
|
300
|
+
intent: z.enum(["draft", "publish"]),
|
|
301
|
+
value: managedResourceObject,
|
|
302
|
+
...mutationContext.shape,
|
|
303
|
+
})
|
|
304
|
+
.strict(),
|
|
305
|
+
z
|
|
306
|
+
.object({
|
|
307
|
+
kind: z.literal("guild.module.resource.update"),
|
|
308
|
+
guildId: snowflake,
|
|
309
|
+
moduleId,
|
|
310
|
+
resourceType,
|
|
311
|
+
resourceId,
|
|
312
|
+
expectedRevision: z.number().int().min(0),
|
|
313
|
+
value: managedResourceObject,
|
|
314
|
+
...mutationContext.shape,
|
|
315
|
+
})
|
|
316
|
+
.strict(),
|
|
317
|
+
z
|
|
318
|
+
.object({
|
|
319
|
+
kind: z.literal("guild.module.resource.delete"),
|
|
320
|
+
guildId: snowflake,
|
|
321
|
+
moduleId,
|
|
322
|
+
resourceType,
|
|
323
|
+
resourceId,
|
|
324
|
+
expectedRevision: z.number().int().min(0),
|
|
325
|
+
...mutationContext.shape,
|
|
326
|
+
})
|
|
327
|
+
.strict(),
|
|
328
|
+
z
|
|
329
|
+
.object({
|
|
330
|
+
kind: z.literal("guild.module.resource.action.execute"),
|
|
331
|
+
guildId: snowflake,
|
|
332
|
+
moduleId,
|
|
333
|
+
resourceType,
|
|
334
|
+
resourceId,
|
|
335
|
+
actionId: resourceType,
|
|
336
|
+
expectedRevision: z.number().int().min(0),
|
|
337
|
+
value: managedResourceObject,
|
|
338
|
+
...mutationContext.shape,
|
|
339
|
+
})
|
|
340
|
+
.strict(),
|
|
341
|
+
z
|
|
342
|
+
.object({
|
|
343
|
+
kind: z.literal("support.notification.send"),
|
|
344
|
+
guildId: snowflake,
|
|
345
|
+
notificationId: z.uuid(),
|
|
346
|
+
reference: z.string().trim().min(1).max(40),
|
|
347
|
+
caseType: z.enum(["bug", "general_support", "security"]),
|
|
348
|
+
categoryOrModule: z.string().trim().min(1).max(100),
|
|
349
|
+
reportedSeverity: z
|
|
350
|
+
.enum(["low", "medium", "high", "critical"])
|
|
351
|
+
.nullable(),
|
|
352
|
+
source: z.enum([
|
|
353
|
+
"dashboard_bug",
|
|
354
|
+
"dashboard_support",
|
|
355
|
+
"dashboard_security",
|
|
356
|
+
"discord_command",
|
|
357
|
+
]),
|
|
358
|
+
})
|
|
359
|
+
.strict(),
|
|
196
360
|
z.object({ kind: z.literal("privacy.providers.list") }).strict(),
|
|
197
361
|
z
|
|
198
362
|
.object({
|
|
@@ -269,6 +433,19 @@ const shardCloudOperationSchema = z.discriminatedUnion("kind", [
|
|
|
269
433
|
.strict(),
|
|
270
434
|
z.object({ kind: z.literal("cloud.disconnect") }).strict(),
|
|
271
435
|
z.object({ kind: z.literal("cloud.rotate") }).strict(),
|
|
436
|
+
z
|
|
437
|
+
.object({
|
|
438
|
+
kind: z.literal("support.bug.create"),
|
|
439
|
+
reporterDiscordUserId: snowflake,
|
|
440
|
+
guildId: snowflake,
|
|
441
|
+
moduleOrArea: z.string().trim().min(1).max(100),
|
|
442
|
+
summary: z.string().trim().min(10).max(200),
|
|
443
|
+
whatHappened: z.string().trim().min(10).max(1_000),
|
|
444
|
+
extraDetail: z.string().trim().min(1).max(1_000).optional(),
|
|
445
|
+
interactionId: snowflake,
|
|
446
|
+
correlationId: z.uuid(),
|
|
447
|
+
})
|
|
448
|
+
.strict(),
|
|
272
449
|
]);
|
|
273
450
|
export const shardCloudRequestSchema = z
|
|
274
451
|
.object({
|
|
@@ -315,10 +492,53 @@ export const shardOperationsHealthResponseSchema = z
|
|
|
315
492
|
health: shardHealthSnapshotSchema,
|
|
316
493
|
})
|
|
317
494
|
.strict();
|
|
495
|
+
export const shardScheduledTaskRequestSchema = z
|
|
496
|
+
.object({
|
|
497
|
+
version: z.literal(SHARD_CONTRACT_VERSION),
|
|
498
|
+
type: z.literal("helyx.scheduled.request"),
|
|
499
|
+
requestId: z.uuid(),
|
|
500
|
+
deadlineAt: dateTime,
|
|
501
|
+
identity: shardIdentitySchema,
|
|
502
|
+
task: z.discriminatedUnion("delivery", [
|
|
503
|
+
z
|
|
504
|
+
.object({
|
|
505
|
+
delivery: z.literal("one-shot"),
|
|
506
|
+
taskId: z.uuid(),
|
|
507
|
+
moduleId,
|
|
508
|
+
taskKind: z.string().regex(/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/u),
|
|
509
|
+
taskVersion: z.number().int().positive(),
|
|
510
|
+
guildId: snowflake,
|
|
511
|
+
payload: scheduledTaskPayload,
|
|
512
|
+
})
|
|
513
|
+
.strict(),
|
|
514
|
+
z
|
|
515
|
+
.object({
|
|
516
|
+
delivery: z.literal("coalesced"),
|
|
517
|
+
taskId: z.uuid(),
|
|
518
|
+
moduleId,
|
|
519
|
+
taskKind: z.string().regex(/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/u),
|
|
520
|
+
taskVersion: z.number().int().positive(),
|
|
521
|
+
guildId: snowflake,
|
|
522
|
+
payload: scheduledTaskPayload,
|
|
523
|
+
claimedGeneration: z.number().int().positive(),
|
|
524
|
+
})
|
|
525
|
+
.strict(),
|
|
526
|
+
]),
|
|
527
|
+
})
|
|
528
|
+
.strict();
|
|
529
|
+
export const shardScheduledTaskResponseSchema = z
|
|
530
|
+
.object({
|
|
531
|
+
version: z.literal(SHARD_CONTRACT_VERSION),
|
|
532
|
+
type: z.literal("helyx.scheduled.response"),
|
|
533
|
+
requestId: z.uuid(),
|
|
534
|
+
result: scheduledTaskResult,
|
|
535
|
+
})
|
|
536
|
+
.strict();
|
|
318
537
|
export const supervisorMessageSchema = z.discriminatedUnion("type", [
|
|
319
538
|
shardControlRequestSchema,
|
|
320
539
|
shardCloudResponseSchema,
|
|
321
540
|
shardOperationsHealthResponseSchema,
|
|
541
|
+
shardScheduledTaskRequestSchema,
|
|
322
542
|
z
|
|
323
543
|
.object({
|
|
324
544
|
version: z.literal(SHARD_CONTRACT_VERSION),
|
|
@@ -344,6 +564,7 @@ export const workerMessageSchema = z.discriminatedUnion("type", [
|
|
|
344
564
|
shardControlResponseSchema,
|
|
345
565
|
shardCloudRequestSchema,
|
|
346
566
|
shardOperationsHealthRequestSchema,
|
|
567
|
+
shardScheduledTaskResponseSchema,
|
|
347
568
|
z
|
|
348
569
|
.object({
|
|
349
570
|
version: z.literal(SHARD_CONTRACT_VERSION),
|
|
@@ -418,6 +639,103 @@ export async function executeShardControlOperation(handler, operation) {
|
|
|
418
639
|
correlationId: operation.correlationId,
|
|
419
640
|
});
|
|
420
641
|
}
|
|
642
|
+
case "guild.module.resource.list":
|
|
643
|
+
return handler.listManagedResources({
|
|
644
|
+
guildId: operation.guildId,
|
|
645
|
+
moduleId: operation.moduleId,
|
|
646
|
+
resourceType: operation.resourceType,
|
|
647
|
+
...(operation.cursor === undefined ? {} : { cursor: operation.cursor }),
|
|
648
|
+
...(operation.search === undefined ? {} : { search: operation.search }),
|
|
649
|
+
...(operation.filters === undefined
|
|
650
|
+
? {}
|
|
651
|
+
: { filters: operation.filters }),
|
|
652
|
+
limit: operation.limit,
|
|
653
|
+
actorUserId: operation.actorUserId,
|
|
654
|
+
correlationId: operation.correlationId,
|
|
655
|
+
});
|
|
656
|
+
case "guild.module.resource.read":
|
|
657
|
+
return handler.readManagedResource({
|
|
658
|
+
guildId: operation.guildId,
|
|
659
|
+
moduleId: operation.moduleId,
|
|
660
|
+
resourceType: operation.resourceType,
|
|
661
|
+
resourceId: operation.resourceId,
|
|
662
|
+
actorUserId: operation.actorUserId,
|
|
663
|
+
correlationId: operation.correlationId,
|
|
664
|
+
});
|
|
665
|
+
case "guild.module.resource.relation.list":
|
|
666
|
+
return handler.listManagedResourceRelation({
|
|
667
|
+
guildId: operation.guildId,
|
|
668
|
+
moduleId: operation.moduleId,
|
|
669
|
+
resourceType: operation.resourceType,
|
|
670
|
+
resourceId: operation.resourceId,
|
|
671
|
+
relationId: operation.relationId,
|
|
672
|
+
...(operation.cursor === undefined ? {} : { cursor: operation.cursor }),
|
|
673
|
+
limit: operation.limit,
|
|
674
|
+
actorUserId: operation.actorUserId,
|
|
675
|
+
correlationId: operation.correlationId,
|
|
676
|
+
});
|
|
677
|
+
case "guild.module.resource.preview":
|
|
678
|
+
return handler.previewManagedResource({
|
|
679
|
+
guildId: operation.guildId,
|
|
680
|
+
moduleId: operation.moduleId,
|
|
681
|
+
resourceType: operation.resourceType,
|
|
682
|
+
value: operation.value,
|
|
683
|
+
actorUserId: operation.actorUserId,
|
|
684
|
+
correlationId: operation.correlationId,
|
|
685
|
+
});
|
|
686
|
+
case "guild.module.resource.create":
|
|
687
|
+
return handler.createManagedResource({
|
|
688
|
+
guildId: operation.guildId,
|
|
689
|
+
moduleId: operation.moduleId,
|
|
690
|
+
resourceType: operation.resourceType,
|
|
691
|
+
intent: operation.intent,
|
|
692
|
+
value: operation.value,
|
|
693
|
+
actorUserId: operation.actorUserId,
|
|
694
|
+
correlationId: operation.correlationId,
|
|
695
|
+
});
|
|
696
|
+
case "guild.module.resource.update":
|
|
697
|
+
return handler.updateManagedResource({
|
|
698
|
+
guildId: operation.guildId,
|
|
699
|
+
moduleId: operation.moduleId,
|
|
700
|
+
resourceType: operation.resourceType,
|
|
701
|
+
resourceId: operation.resourceId,
|
|
702
|
+
expectedRevision: operation.expectedRevision,
|
|
703
|
+
value: operation.value,
|
|
704
|
+
actorUserId: operation.actorUserId,
|
|
705
|
+
correlationId: operation.correlationId,
|
|
706
|
+
});
|
|
707
|
+
case "guild.module.resource.delete":
|
|
708
|
+
return handler.deleteManagedResource({
|
|
709
|
+
guildId: operation.guildId,
|
|
710
|
+
moduleId: operation.moduleId,
|
|
711
|
+
resourceType: operation.resourceType,
|
|
712
|
+
resourceId: operation.resourceId,
|
|
713
|
+
expectedRevision: operation.expectedRevision,
|
|
714
|
+
actorUserId: operation.actorUserId,
|
|
715
|
+
correlationId: operation.correlationId,
|
|
716
|
+
});
|
|
717
|
+
case "guild.module.resource.action.execute":
|
|
718
|
+
return handler.executeManagedResourceAction({
|
|
719
|
+
guildId: operation.guildId,
|
|
720
|
+
moduleId: operation.moduleId,
|
|
721
|
+
resourceType: operation.resourceType,
|
|
722
|
+
resourceId: operation.resourceId,
|
|
723
|
+
actionId: operation.actionId,
|
|
724
|
+
expectedRevision: operation.expectedRevision,
|
|
725
|
+
value: operation.value,
|
|
726
|
+
actorUserId: operation.actorUserId,
|
|
727
|
+
correlationId: operation.correlationId,
|
|
728
|
+
});
|
|
729
|
+
case "support.notification.send": {
|
|
730
|
+
return handler.sendSupportNotification({
|
|
731
|
+
notificationId: operation.notificationId,
|
|
732
|
+
reference: operation.reference,
|
|
733
|
+
caseType: operation.caseType,
|
|
734
|
+
categoryOrModule: operation.categoryOrModule,
|
|
735
|
+
reportedSeverity: operation.reportedSeverity,
|
|
736
|
+
source: operation.source,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
421
739
|
case "privacy.providers.list":
|
|
422
740
|
return handler.listPrivacyProviders();
|
|
423
741
|
case "privacy.capture": {
|
|
@@ -442,4 +760,32 @@ export async function executeShardControlOperation(handler, operation) {
|
|
|
442
760
|
}
|
|
443
761
|
}
|
|
444
762
|
}
|
|
763
|
+
function isBoundedManagedResourceValue(value, depth = 0, budget = { keys: 0 }) {
|
|
764
|
+
if (depth > 8)
|
|
765
|
+
return false;
|
|
766
|
+
if (value === null ||
|
|
767
|
+
typeof value === "boolean" ||
|
|
768
|
+
(typeof value === "number" && Number.isFinite(value)))
|
|
769
|
+
return true;
|
|
770
|
+
if (typeof value === "string")
|
|
771
|
+
return value.length <= 4_000;
|
|
772
|
+
if (Array.isArray(value))
|
|
773
|
+
return (value.length <= 100 &&
|
|
774
|
+
value.every((item) => isBoundedManagedResourceValue(item, depth + 1, budget)));
|
|
775
|
+
if (typeof value !== "object")
|
|
776
|
+
return false;
|
|
777
|
+
const prototype = Object.getPrototypeOf(value);
|
|
778
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
779
|
+
return false;
|
|
780
|
+
const entries = Object.entries(value);
|
|
781
|
+
budget.keys += entries.length;
|
|
782
|
+
return (entries.length <= 100 &&
|
|
783
|
+
budget.keys <= 500 &&
|
|
784
|
+
entries.every(([key, item]) => key.length >= 1 &&
|
|
785
|
+
key.length <= 100 &&
|
|
786
|
+
isBoundedManagedResourceValue(item, depth + 1, budget)));
|
|
787
|
+
}
|
|
788
|
+
function hasBoundedManagedResourceBytes(value) {
|
|
789
|
+
return (new TextEncoder().encode(JSON.stringify(value)).byteLength <= 48 * 1_024);
|
|
790
|
+
}
|
|
445
791
|
//# sourceMappingURL=shard-contracts.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { type ShardControlRequest, type ShardControlResponse, type ShardCloudRequest, type ShardCloudResponse, type ShardIdentity, type ShardProcessHealth } from "./shard-contracts.js";
|
|
3
|
+
import { type ShardControlRequest, type ShardControlResponse, type ShardCloudRequest, type ShardCloudResponse, type ShardIdentity, type ShardProcessHealth, type ShardScheduledTaskRequest, type ShardScheduledTaskResponse } from "./shard-contracts.js";
|
|
4
4
|
declare const generationDescriptorSchema: z.ZodObject<{
|
|
5
5
|
version: z.ZodLiteral<1>;
|
|
6
6
|
applicationId: z.ZodString;
|
|
@@ -30,7 +30,7 @@ declare const leaderLeaseSchema: z.ZodObject<{
|
|
|
30
30
|
export type ShardLeaderLease = z.infer<typeof leaderLeaseSchema>;
|
|
31
31
|
export interface ShardCoordinator {
|
|
32
32
|
readonly supervisorId: ReturnType<typeof randomUUID>;
|
|
33
|
-
start(handler: (targetShardId: number, request: ShardControlRequest) => Promise<ShardControlResponse>, leaderHandler?: (request: ShardControlRequest) => Promise<ShardControlResponse>, cloudHandler?: (request: ShardCloudRequest) => Promise<ShardCloudResponse>): Promise<void>;
|
|
33
|
+
start(handler: (targetShardId: number, request: ShardControlRequest) => Promise<ShardControlResponse>, leaderHandler?: (request: ShardControlRequest) => Promise<ShardControlResponse>, cloudHandler?: (request: ShardCloudRequest) => Promise<ShardCloudResponse>, scheduledHandler?: (targetShardId: number, request: ShardScheduledTaskRequest) => Promise<ShardScheduledTaskResponse>): Promise<void>;
|
|
34
34
|
checkReady(): Promise<void>;
|
|
35
35
|
assertGeneration(descriptor: ShardGenerationDescriptor, proposedDeploymentOrder: number): Promise<number>;
|
|
36
36
|
refreshGeneration(descriptor: ShardGenerationDescriptor, deploymentOrder: number): Promise<boolean>;
|
|
@@ -44,6 +44,7 @@ export interface ShardCoordinator {
|
|
|
44
44
|
releaseLeader(lease: ShardLeaderLease): Promise<boolean>;
|
|
45
45
|
getLeaderLease(): Promise<ShardLeaderLease | undefined>;
|
|
46
46
|
routeRemote(lease: ShardLease, request: ShardControlRequest): Promise<ShardControlResponse>;
|
|
47
|
+
routeRemoteScheduled(lease: ShardLease, request: ShardScheduledTaskRequest): Promise<ShardScheduledTaskResponse>;
|
|
47
48
|
routeLeader(lease: ShardLeaderLease, request: ShardControlRequest): Promise<ShardControlResponse>;
|
|
48
49
|
routeLeaderCloud(lease: ShardLeaderLease, request: ShardCloudRequest): Promise<ShardCloudResponse>;
|
|
49
50
|
reserveSessionStarts(count: number, available: number, resetAfterMs: number): Promise<boolean>;
|
|
@@ -61,7 +62,7 @@ export declare class RedisShardCoordinator implements ShardCoordinator {
|
|
|
61
62
|
constructor(redisUrl: string, namespace: string, options?: {
|
|
62
63
|
onError?: (error: Error) => void;
|
|
63
64
|
});
|
|
64
|
-
start(handler: (targetShardId: number, request: ShardControlRequest) => Promise<ShardControlResponse>, leaderHandler?: (request: ShardControlRequest) => Promise<ShardControlResponse>, cloudHandler?: (request: ShardCloudRequest) => Promise<ShardCloudResponse>): Promise<void>;
|
|
65
|
+
start(handler: (targetShardId: number, request: ShardControlRequest) => Promise<ShardControlResponse>, leaderHandler?: (request: ShardControlRequest) => Promise<ShardControlResponse>, cloudHandler?: (request: ShardCloudRequest) => Promise<ShardCloudResponse>, scheduledHandler?: (targetShardId: number, request: ShardScheduledTaskRequest) => Promise<ShardScheduledTaskResponse>): Promise<void>;
|
|
65
66
|
checkReady(): Promise<void>;
|
|
66
67
|
assertGeneration(descriptor: ShardGenerationDescriptor, proposedDeploymentOrder: number): Promise<number>;
|
|
67
68
|
refreshGeneration(descriptor: ShardGenerationDescriptor, deploymentOrder: number): Promise<boolean>;
|
|
@@ -75,6 +76,7 @@ export declare class RedisShardCoordinator implements ShardCoordinator {
|
|
|
75
76
|
releaseLeader(lease: ShardLeaderLease): Promise<boolean>;
|
|
76
77
|
getLeaderLease(): Promise<ShardLeaderLease | undefined>;
|
|
77
78
|
routeRemote(lease: ShardLease, request: ShardControlRequest): Promise<ShardControlResponse>;
|
|
79
|
+
routeRemoteScheduled(lease: ShardLease, request: ShardScheduledTaskRequest): Promise<ShardScheduledTaskResponse>;
|
|
78
80
|
routeLeader(lease: ShardLeaderLease, request: ShardControlRequest): Promise<ShardControlResponse>;
|
|
79
81
|
routeLeaderCloud(lease: ShardLeaderLease, request: ShardCloudRequest): Promise<ShardCloudResponse>;
|
|
80
82
|
reserveSessionStarts(count: number, available: number, resetAfterMs: number): Promise<boolean>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { createClient } from "redis";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { SHARD_CONTROL_MAX_BYTES, assertShardMessageSize, shardCloudRequestSchema, shardCloudResponseSchema, shardControlRequestSchema, shardControlResponseSchema, shardIdentitySchema, shardProcessHealthSchema, } from "./shard-contracts.js";
|
|
4
|
+
import { SHARD_CONTROL_MAX_BYTES, assertShardMessageSize, shardCloudRequestSchema, shardCloudResponseSchema, shardControlRequestSchema, shardControlResponseSchema, shardIdentitySchema, shardProcessHealthSchema, shardScheduledTaskRequestSchema, shardScheduledTaskResponseSchema, } from "./shard-contracts.js";
|
|
5
5
|
const LEASE_TTL_MS = 30_000;
|
|
6
6
|
const HEALTH_TTL_MS = 30_000;
|
|
7
7
|
const GENERATION_TTL_MS = 30_000;
|
|
@@ -52,6 +52,15 @@ const remoteRequestSchema = z.discriminatedUnion("target", [
|
|
|
52
52
|
request: shardControlRequestSchema,
|
|
53
53
|
})
|
|
54
54
|
.strict(),
|
|
55
|
+
z
|
|
56
|
+
.object({
|
|
57
|
+
version: z.literal(1),
|
|
58
|
+
type: z.literal("helyx.remote.request"),
|
|
59
|
+
target: z.literal("scheduled"),
|
|
60
|
+
targetShardId: z.number().int().min(0).max(4_095),
|
|
61
|
+
request: shardScheduledTaskRequestSchema,
|
|
62
|
+
})
|
|
63
|
+
.strict(),
|
|
55
64
|
z
|
|
56
65
|
.object({
|
|
57
66
|
version: z.literal(1),
|
|
@@ -86,6 +95,7 @@ export class RedisShardCoordinator {
|
|
|
86
95
|
#handler;
|
|
87
96
|
#leaderHandler;
|
|
88
97
|
#cloudHandler;
|
|
98
|
+
#scheduledHandler;
|
|
89
99
|
constructor(redisUrl, namespace, options = {}) {
|
|
90
100
|
this.#prefix = `helyx:shard:v1:${namespace}`;
|
|
91
101
|
this.#command = createClient({ url: redisUrl });
|
|
@@ -94,10 +104,11 @@ export class RedisShardCoordinator {
|
|
|
94
104
|
this.#command.on("error", (error) => this.#report(error));
|
|
95
105
|
this.#subscriber.on("error", (error) => this.#report(error));
|
|
96
106
|
}
|
|
97
|
-
async start(handler, leaderHandler, cloudHandler) {
|
|
107
|
+
async start(handler, leaderHandler, cloudHandler, scheduledHandler) {
|
|
98
108
|
this.#handler = handler;
|
|
99
109
|
this.#leaderHandler = leaderHandler;
|
|
100
110
|
this.#cloudHandler = cloudHandler;
|
|
111
|
+
this.#scheduledHandler = scheduledHandler;
|
|
101
112
|
await Promise.all([this.#command.connect(), this.#subscriber.connect()]);
|
|
102
113
|
await this.#subscriber.subscribe(this.#requestChannel(), (raw) => {
|
|
103
114
|
void this.#handleRemoteRequest(raw).catch((error) => this.#report(error));
|
|
@@ -305,6 +316,26 @@ export class RedisShardCoordinator {
|
|
|
305
316
|
}
|
|
306
317
|
throw new Error("Shard control request timed out");
|
|
307
318
|
}
|
|
319
|
+
async routeRemoteScheduled(lease, request) {
|
|
320
|
+
const acceptedLease = leaseSchema.parse(lease);
|
|
321
|
+
const acceptedRequest = shardScheduledTaskRequestSchema.parse(request);
|
|
322
|
+
if (acceptedRequest.identity.generation !== acceptedLease.generation ||
|
|
323
|
+
acceptedRequest.identity.fencingToken !== acceptedLease.fencingToken ||
|
|
324
|
+
acceptedRequest.identity.shardId !== acceptedLease.shardId)
|
|
325
|
+
throw new Error("Scheduled shard route ownership changed");
|
|
326
|
+
const envelope = remoteRequestSchema.parse({
|
|
327
|
+
version: 1,
|
|
328
|
+
type: "helyx.remote.request",
|
|
329
|
+
target: "scheduled",
|
|
330
|
+
targetShardId: acceptedLease.shardId,
|
|
331
|
+
request: acceptedRequest,
|
|
332
|
+
});
|
|
333
|
+
assertShardMessageSize(envelope);
|
|
334
|
+
const listeners = await this.#command.publish(this.#requestChannel(acceptedLease.supervisorId), JSON.stringify(envelope));
|
|
335
|
+
if (listeners < 1)
|
|
336
|
+
throw new Error("Owning shard supervisor is unavailable");
|
|
337
|
+
return this.#waitForScheduledResponse(acceptedRequest);
|
|
338
|
+
}
|
|
308
339
|
async routeLeader(lease, request) {
|
|
309
340
|
const acceptedLease = leaderLeaseSchema.parse(lease);
|
|
310
341
|
const acceptedRequest = shardControlRequestSchema.parse(request);
|
|
@@ -482,6 +513,38 @@ export class RedisShardCoordinator {
|
|
|
482
513
|
await this.#writeResponse(response);
|
|
483
514
|
return;
|
|
484
515
|
}
|
|
516
|
+
if (envelope.data.target === "scheduled") {
|
|
517
|
+
const { targetShardId, request } = envelope.data;
|
|
518
|
+
const lease = await this.getShardLease(targetShardId);
|
|
519
|
+
if (!lease ||
|
|
520
|
+
lease.supervisorId !== this.supervisorId ||
|
|
521
|
+
lease.generation !== request.identity.generation ||
|
|
522
|
+
lease.fencingToken !== request.identity.fencingToken ||
|
|
523
|
+
lease.shardId !== request.identity.shardId ||
|
|
524
|
+
!this.#scheduledHandler)
|
|
525
|
+
return this.#writeScheduledResponse({
|
|
526
|
+
version: 1,
|
|
527
|
+
type: "helyx.scheduled.response",
|
|
528
|
+
requestId: request.requestId,
|
|
529
|
+
result: {
|
|
530
|
+
outcome: "deferred",
|
|
531
|
+
delayMs: 1_000,
|
|
532
|
+
safeCode: "shard_ownership_changed",
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
const response = await this.#scheduledHandler(targetShardId, request).catch(() => ({
|
|
536
|
+
version: 1,
|
|
537
|
+
type: "helyx.scheduled.response",
|
|
538
|
+
requestId: request.requestId,
|
|
539
|
+
result: {
|
|
540
|
+
outcome: "deferred",
|
|
541
|
+
delayMs: 1_000,
|
|
542
|
+
safeCode: "shard_unavailable",
|
|
543
|
+
},
|
|
544
|
+
}));
|
|
545
|
+
await this.#writeScheduledResponse(response);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
485
548
|
const { targetShardId, request } = envelope.data;
|
|
486
549
|
const lease = await this.getShardLease(targetShardId);
|
|
487
550
|
if (!lease ||
|
|
@@ -516,6 +579,11 @@ export class RedisShardCoordinator {
|
|
|
516
579
|
assertShardMessageSize(accepted);
|
|
517
580
|
await this.#command.set(this.#responseKey(accepted.requestId), JSON.stringify(accepted), { EX: RESPONSE_TTL_SECONDS });
|
|
518
581
|
}
|
|
582
|
+
async #writeScheduledResponse(response) {
|
|
583
|
+
const accepted = shardScheduledTaskResponseSchema.parse(response);
|
|
584
|
+
assertShardMessageSize(accepted);
|
|
585
|
+
await this.#command.set(this.#responseKey(accepted.requestId), JSON.stringify(accepted), { EX: RESPONSE_TTL_SECONDS });
|
|
586
|
+
}
|
|
519
587
|
async #waitForResponse(request) {
|
|
520
588
|
const deadline = new Date(request.deadlineAt).getTime();
|
|
521
589
|
const responseKey = this.#responseKey(request.requestId);
|
|
@@ -531,6 +599,21 @@ export class RedisShardCoordinator {
|
|
|
531
599
|
}
|
|
532
600
|
throw new Error("Shard control request timed out");
|
|
533
601
|
}
|
|
602
|
+
async #waitForScheduledResponse(request) {
|
|
603
|
+
const deadline = new Date(request.deadlineAt).getTime();
|
|
604
|
+
const responseKey = this.#responseKey(request.requestId);
|
|
605
|
+
while (Date.now() < deadline) {
|
|
606
|
+
const raw = await this.#command.get(responseKey);
|
|
607
|
+
if (raw) {
|
|
608
|
+
await this.#command.del(responseKey);
|
|
609
|
+
const parsed = shardScheduledTaskResponseSchema.parse(parseJson(raw));
|
|
610
|
+
assertShardMessageSize(parsed);
|
|
611
|
+
return parsed;
|
|
612
|
+
}
|
|
613
|
+
await wait(ROUTE_POLL_MS);
|
|
614
|
+
}
|
|
615
|
+
throw new Error("Scheduled shard request timed out");
|
|
616
|
+
}
|
|
534
617
|
#generationKey() {
|
|
535
618
|
return `${this.#prefix}:generation`;
|
|
536
619
|
}
|
package/dist/shard-router.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare class ShardControlRouter implements BotControlHandler {
|
|
|
13
13
|
privacy?: Pick<BotControlHandler, "listPrivacyProviders" | "capturePrivacy" | "erasePrivacy">;
|
|
14
14
|
localLeaderLease?: ShardLeaderLease;
|
|
15
15
|
coordinator?: ShardCoordinator;
|
|
16
|
+
notificationGuildId?: string;
|
|
16
17
|
});
|
|
17
18
|
getGuildState(guildId: string): ReturnType<BotControlHandler["getGuildState"]>;
|
|
18
19
|
setModuleStatus(input: Parameters<BotControlHandler["setModuleStatus"]>[0]): ReturnType<BotControlHandler["setModuleStatus"]>;
|
|
@@ -20,6 +21,15 @@ export declare class ShardControlRouter implements BotControlHandler {
|
|
|
20
21
|
updatePermissions(input: Parameters<BotControlHandler["updatePermissions"]>[0]): ReturnType<BotControlHandler["updatePermissions"]>;
|
|
21
22
|
updateLoggingPolicy(input: Parameters<BotControlHandler["updateLoggingPolicy"]>[0]): ReturnType<BotControlHandler["updateLoggingPolicy"]>;
|
|
22
23
|
executeDashboardAction(input: Parameters<BotControlHandler["executeDashboardAction"]>[0]): ReturnType<BotControlHandler["executeDashboardAction"]>;
|
|
24
|
+
listManagedResources(input: Parameters<BotControlHandler["listManagedResources"]>[0]): ReturnType<BotControlHandler["listManagedResources"]>;
|
|
25
|
+
readManagedResource(input: Parameters<BotControlHandler["readManagedResource"]>[0]): ReturnType<BotControlHandler["readManagedResource"]>;
|
|
26
|
+
listManagedResourceRelation(input: Parameters<BotControlHandler["listManagedResourceRelation"]>[0]): ReturnType<BotControlHandler["listManagedResourceRelation"]>;
|
|
27
|
+
previewManagedResource(input: Parameters<BotControlHandler["previewManagedResource"]>[0]): ReturnType<BotControlHandler["previewManagedResource"]>;
|
|
28
|
+
createManagedResource(input: Parameters<BotControlHandler["createManagedResource"]>[0]): ReturnType<BotControlHandler["createManagedResource"]>;
|
|
29
|
+
updateManagedResource(input: Parameters<BotControlHandler["updateManagedResource"]>[0]): ReturnType<BotControlHandler["updateManagedResource"]>;
|
|
30
|
+
deleteManagedResource(input: Parameters<BotControlHandler["deleteManagedResource"]>[0]): ReturnType<BotControlHandler["deleteManagedResource"]>;
|
|
31
|
+
executeManagedResourceAction(input: Parameters<BotControlHandler["executeManagedResourceAction"]>[0]): ReturnType<BotControlHandler["executeManagedResourceAction"]>;
|
|
32
|
+
sendSupportNotification(input: Parameters<BotControlHandler["sendSupportNotification"]>[0]): ReturnType<BotControlHandler["sendSupportNotification"]>;
|
|
23
33
|
listPrivacyProviders(): ReturnType<BotControlHandler["listPrivacyProviders"]>;
|
|
24
34
|
capturePrivacy(input: Parameters<BotControlHandler["capturePrivacy"]>[0]): ReturnType<BotControlHandler["capturePrivacy"]>;
|
|
25
35
|
erasePrivacy(input: Parameters<BotControlHandler["erasePrivacy"]>[0]): ReturnType<BotControlHandler["erasePrivacy"]>;
|