@helyx/bot 0.4.33 → 0.4.36
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 +45 -0
- package/dist/cloud-management-module.js +7 -1
- package/dist/control-server.d.ts +6 -7
- package/dist/control-server.js +38 -45
- package/dist/dashboard-commands.d.ts +10 -0
- package/dist/dashboard-commands.js +47 -0
- package/dist/developer-operations-module.js +5 -1
- package/dist/discord-guild-health.d.ts +4 -3
- package/dist/discord-guild-health.js +6 -3
- package/dist/events.js +38 -16
- package/dist/index.js +43 -3
- package/dist/interactions.d.ts +3 -2
- package/dist/interactions.js +132 -45
- package/dist/internal-module-manifest.d.ts +13 -0
- package/dist/internal-module-manifest.js +14 -0
- package/dist/management-module.js +7 -2
- package/dist/module-availability-control.d.ts +24 -0
- package/dist/module-availability-control.js +61 -0
- package/dist/modules.js +24 -1
- package/dist/nexus-commands.d.ts +13 -0
- package/dist/nexus-commands.js +100 -0
- package/dist/numeric-barriers.d.ts +11 -0
- package/dist/numeric-barriers.js +14 -0
- package/dist/numeric-write-behind-proxy.d.ts +4 -4
- package/dist/numeric-write-behind-proxy.js +45 -24
- package/dist/numeric-write-behind-redis.d.ts +4 -4
- package/dist/numeric-write-behind-redis.js +132 -70
- package/dist/oauth-module.js +3 -1
- package/dist/permissions-module.js +18 -5
- package/dist/renewable-lease.d.ts +16 -0
- package/dist/renewable-lease.js +66 -0
- package/dist/scheduled-task-runner.js +15 -1
- package/dist/shard-router.d.ts +2 -0
- package/dist/shard-router.js +5 -0
- package/dist/shard-supervisor.d.ts +5 -0
- package/dist/shard-supervisor.js +63 -4
- package/dist/shard-worker.d.ts +7 -1
- package/dist/shard-worker.js +4 -1
- package/dist/support-module.js +3 -1
- package/package.json +5 -4
|
@@ -2,6 +2,7 @@ import {} from "@helyx/sdk";
|
|
|
2
2
|
import {} from "@helyx/platform";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { createClient } from "redis";
|
|
5
|
+
import { RenewableLease } from "./renewable-lease.js";
|
|
5
6
|
const REDIS_FLUSH_INTERVAL_MS = 30_000;
|
|
6
7
|
const REDIS_BATCH_SIZE = 100;
|
|
7
8
|
const REDIS_CAPACITY = 10_000;
|
|
@@ -189,40 +190,13 @@ export class RedisNumericWriteBehindService {
|
|
|
189
190
|
return this.#flush(input, "", "", true);
|
|
190
191
|
}
|
|
191
192
|
async runMemberBarrier(input, operation) {
|
|
192
|
-
|
|
193
|
-
const stopRenewal = this.#startBarrierRenewal(input, "member", input.guildId, input.subjectId, barrierId);
|
|
194
|
-
try {
|
|
195
|
-
await this.flushMember(input);
|
|
196
|
-
return await operation();
|
|
197
|
-
}
|
|
198
|
-
finally {
|
|
199
|
-
stopRenewal();
|
|
200
|
-
await this.#releaseBarrier(input, "member", input.guildId, input.subjectId, barrierId);
|
|
201
|
-
}
|
|
193
|
+
return this.#runBarrier(input, "member", input.guildId, input.subjectId, () => this.flushMember(input), operation);
|
|
202
194
|
}
|
|
203
195
|
async runGuildBarrier(input, operation) {
|
|
204
|
-
|
|
205
|
-
const stopRenewal = this.#startBarrierRenewal(input, "guild", input.guildId, "", barrierId);
|
|
206
|
-
try {
|
|
207
|
-
await this.flushGuild(input);
|
|
208
|
-
return await operation();
|
|
209
|
-
}
|
|
210
|
-
finally {
|
|
211
|
-
stopRenewal();
|
|
212
|
-
await this.#releaseBarrier(input, "guild", input.guildId, "", barrierId);
|
|
213
|
-
}
|
|
196
|
+
return this.#runBarrier(input, "guild", input.guildId, "", () => this.flushGuild(input), operation);
|
|
214
197
|
}
|
|
215
198
|
async runSubjectBarrier(input, operation) {
|
|
216
|
-
|
|
217
|
-
const stopRenewal = this.#startBarrierRenewal(input, "subject", "", input.subjectId, barrierId);
|
|
218
|
-
try {
|
|
219
|
-
await this.flushSubject(input);
|
|
220
|
-
return await operation();
|
|
221
|
-
}
|
|
222
|
-
finally {
|
|
223
|
-
stopRenewal();
|
|
224
|
-
await this.#releaseBarrier(input, "subject", "", input.subjectId, barrierId);
|
|
225
|
-
}
|
|
199
|
+
return this.#runBarrier(input, "subject", "", input.subjectId, () => this.flushSubject(input), operation);
|
|
226
200
|
}
|
|
227
201
|
async suppressSubject(input) {
|
|
228
202
|
this.#assertAvailable(input);
|
|
@@ -420,16 +394,17 @@ export class RedisNumericWriteBehindService {
|
|
|
420
394
|
async #drainRegistration(registration, guildId, subjectId, force) {
|
|
421
395
|
while (true) {
|
|
422
396
|
const proposedOperationId = randomUUID();
|
|
397
|
+
const proposedClaimToken = randomUUID();
|
|
423
398
|
const raw = await this.#redis.eval(CLAIM_SCRIPT, {
|
|
424
399
|
keys: keysFor(this.#prefix, registration),
|
|
425
400
|
arguments: [
|
|
426
401
|
String(Date.now()),
|
|
427
402
|
String(REDIS_CLAIM_LEASE_MS),
|
|
428
403
|
proposedOperationId,
|
|
404
|
+
proposedClaimToken,
|
|
429
405
|
String(REDIS_BATCH_SIZE),
|
|
430
406
|
guildId,
|
|
431
407
|
subjectId,
|
|
432
|
-
force ? "1" : "0",
|
|
433
408
|
this.#supervisorId ?? "",
|
|
434
409
|
this.#shardLeaseKeyPrefix ?? "",
|
|
435
410
|
String(REDIS_RETRY_RETENTION_MS),
|
|
@@ -451,33 +426,45 @@ export class RedisNumericWriteBehindService {
|
|
|
451
426
|
...this.#health,
|
|
452
427
|
inFlightEntries: claim.entries.length,
|
|
453
428
|
};
|
|
429
|
+
const renewal = new RenewableLease({
|
|
430
|
+
intervalMs: Math.max(250, Math.floor(REDIS_CLAIM_LEASE_MS / 3)),
|
|
431
|
+
renew: () => this.#renewClaim(registration, claim),
|
|
432
|
+
onError: (error) => this.#onError?.(error),
|
|
433
|
+
});
|
|
434
|
+
renewal.start();
|
|
454
435
|
try {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
436
|
+
await renewal.run(async (signal) => {
|
|
437
|
+
const output = await this.#writer.mutateNumericBatch({
|
|
438
|
+
moduleId: registration.moduleId,
|
|
439
|
+
collection: registration.collection,
|
|
440
|
+
operationId: claim.operationId,
|
|
441
|
+
uniqueBy: registration.uniqueBy,
|
|
442
|
+
mutations: claim.entries.map(decodeMutation),
|
|
443
|
+
select: registration.select,
|
|
444
|
+
});
|
|
445
|
+
signal.throwIfAborted();
|
|
446
|
+
if (output.results.length !== claim.entries.length)
|
|
447
|
+
throw new Error("Redis numeric writer result count was invalid");
|
|
448
|
+
const committed = {
|
|
449
|
+
moduleId: registration.moduleId,
|
|
450
|
+
collection: registration.collection,
|
|
451
|
+
operationId: claim.operationId,
|
|
452
|
+
replayed: output.replayed,
|
|
453
|
+
mutations: claim.entries.map(decodeMutation),
|
|
454
|
+
results: output.results,
|
|
455
|
+
};
|
|
456
|
+
for (const subscriber of this.#subscribers.get(scopeKey(registration)) ?? []) {
|
|
457
|
+
signal.throwIfAborted();
|
|
458
|
+
await subscriber(committed, signal);
|
|
459
|
+
}
|
|
460
|
+
signal.throwIfAborted();
|
|
461
|
+
const acknowledged = await this.#redis.eval(ACK_SCRIPT, {
|
|
462
|
+
keys: keysFor(this.#prefix, registration),
|
|
463
|
+
arguments: [claim.operationId, claim.claimToken],
|
|
464
|
+
});
|
|
465
|
+
if (Number(acknowledged) !== 1)
|
|
466
|
+
throw new Error("Redis numeric claim ownership changed");
|
|
478
467
|
});
|
|
479
|
-
if (Number(acknowledged) !== 1)
|
|
480
|
-
throw new Error("Redis numeric claim ownership changed");
|
|
481
468
|
}
|
|
482
469
|
catch (error) {
|
|
483
470
|
const attempt = claim.attempt + 1;
|
|
@@ -485,7 +472,12 @@ export class RedisNumericWriteBehindService {
|
|
|
485
472
|
Math.min(REDIS_RETRY_MAX_MS, REDIS_RETRY_BASE_MS * 2 ** Math.min(claim.attempt, 16));
|
|
486
473
|
await this.#redis.eval(RETRY_SCRIPT, {
|
|
487
474
|
keys: keysFor(this.#prefix, registration),
|
|
488
|
-
arguments: [
|
|
475
|
+
arguments: [
|
|
476
|
+
claim.operationId,
|
|
477
|
+
claim.claimToken,
|
|
478
|
+
String(attempt),
|
|
479
|
+
String(retryAt),
|
|
480
|
+
],
|
|
489
481
|
});
|
|
490
482
|
this.#health = {
|
|
491
483
|
...this.#health,
|
|
@@ -498,6 +490,9 @@ export class RedisNumericWriteBehindService {
|
|
|
498
490
|
throw error;
|
|
499
491
|
return;
|
|
500
492
|
}
|
|
493
|
+
finally {
|
|
494
|
+
await renewal.stop();
|
|
495
|
+
}
|
|
501
496
|
this.#health = {
|
|
502
497
|
...this.#health,
|
|
503
498
|
inFlightEntries: 0,
|
|
@@ -505,6 +500,20 @@ export class RedisNumericWriteBehindService {
|
|
|
505
500
|
};
|
|
506
501
|
}
|
|
507
502
|
}
|
|
503
|
+
async #renewClaim(registration, claim) {
|
|
504
|
+
const now = Date.now();
|
|
505
|
+
const renewed = await this.#redis.eval(RENEW_CLAIM_SCRIPT, {
|
|
506
|
+
keys: keysFor(this.#prefix, registration),
|
|
507
|
+
arguments: [
|
|
508
|
+
claim.operationId,
|
|
509
|
+
claim.claimToken,
|
|
510
|
+
String(now),
|
|
511
|
+
String(now + REDIS_CLAIM_LEASE_MS),
|
|
512
|
+
],
|
|
513
|
+
});
|
|
514
|
+
if (Number(renewed) !== 1)
|
|
515
|
+
throw new Error("Redis numeric claim lease expired");
|
|
516
|
+
}
|
|
508
517
|
async #acquireBarrier(scope, kind, guildId, subjectId) {
|
|
509
518
|
this.#assertAvailable(scope);
|
|
510
519
|
const barrierId = randomUUID();
|
|
@@ -550,12 +559,47 @@ export class RedisNumericWriteBehindService {
|
|
|
550
559
|
if (Number(result) !== 1)
|
|
551
560
|
throw new Error("Numeric write-behind barrier lease expired");
|
|
552
561
|
}
|
|
553
|
-
#
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
562
|
+
async #runBarrier(scope, kind, guildId, subjectId, flush, operation) {
|
|
563
|
+
const barrierId = await this.#acquireBarrier(scope, kind, guildId, subjectId);
|
|
564
|
+
const renewal = new RenewableLease({
|
|
565
|
+
intervalMs: Math.max(250, Math.floor(this.#barrierLeaseMs / 3)),
|
|
566
|
+
renew: () => this.#renewBarrier(scope, kind, guildId, subjectId, barrierId),
|
|
567
|
+
onError: (error) => this.#onError?.(error),
|
|
568
|
+
});
|
|
569
|
+
renewal.start();
|
|
570
|
+
const failures = [];
|
|
571
|
+
let result;
|
|
572
|
+
let completed = false;
|
|
573
|
+
try {
|
|
574
|
+
result = await renewal.run(async (signal) => {
|
|
575
|
+
await flush();
|
|
576
|
+
signal.throwIfAborted();
|
|
577
|
+
return operation(signal);
|
|
578
|
+
});
|
|
579
|
+
completed = true;
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
failures.push(error);
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
await renewal.stop();
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
failures.push(error);
|
|
589
|
+
}
|
|
590
|
+
try {
|
|
591
|
+
await this.#releaseBarrier(scope, kind, guildId, subjectId, barrierId);
|
|
592
|
+
}
|
|
593
|
+
catch (error) {
|
|
594
|
+
failures.push(error);
|
|
595
|
+
}
|
|
596
|
+
if (failures.length === 1)
|
|
597
|
+
throw failures[0];
|
|
598
|
+
if (failures.length > 1)
|
|
599
|
+
throw new AggregateError(failures, "Numeric barrier cleanup failed");
|
|
600
|
+
if (!completed)
|
|
601
|
+
throw new Error("Numeric barrier operation did not complete");
|
|
602
|
+
return result;
|
|
559
603
|
}
|
|
560
604
|
async #waitForSuppression(scope, script, arguments_) {
|
|
561
605
|
const deadline = Date.now() + this.#suppressionDrainTimeoutMs;
|
|
@@ -785,6 +829,9 @@ function parseClaim(raw) {
|
|
|
785
829
|
!("operationId" in parsed) ||
|
|
786
830
|
typeof parsed.operationId !== "string" ||
|
|
787
831
|
!UUID.test(parsed.operationId) ||
|
|
832
|
+
!("claimToken" in parsed) ||
|
|
833
|
+
typeof parsed.claimToken !== "string" ||
|
|
834
|
+
!UUID.test(parsed.claimToken) ||
|
|
788
835
|
!("entries" in parsed) ||
|
|
789
836
|
!Array.isArray(parsed.entries) ||
|
|
790
837
|
parsed.entries.length < 1 ||
|
|
@@ -940,10 +987,10 @@ ${BARRIER_HELPERS_LUA}
|
|
|
940
987
|
local now = tonumber(ARGV[1])
|
|
941
988
|
local lease_ms = tonumber(ARGV[2])
|
|
942
989
|
local proposed_id = ARGV[3]
|
|
943
|
-
local
|
|
944
|
-
local
|
|
945
|
-
local
|
|
946
|
-
local
|
|
990
|
+
local claim_token = ARGV[4]
|
|
991
|
+
local limit = tonumber(ARGV[5])
|
|
992
|
+
local guild_filter = ARGV[6]
|
|
993
|
+
local subject_filter = ARGV[7]
|
|
947
994
|
local owner = ARGV[8]
|
|
948
995
|
local lease_prefix = ARGV[9]
|
|
949
996
|
local retry_retention_ms = tonumber(ARGV[10])
|
|
@@ -958,7 +1005,7 @@ for index = 1, #staged, 2 do
|
|
|
958
1005
|
redis.call('HDEL', KEYS[3], member_key)
|
|
959
1006
|
end
|
|
960
1007
|
end
|
|
961
|
-
local due = redis.call('ZRANGEBYSCORE', KEYS[5], '-inf',
|
|
1008
|
+
local due = redis.call('ZRANGEBYSCORE', KEYS[5], '-inf', now, 'LIMIT', 0, 100)
|
|
962
1009
|
for _, operation_id in ipairs(due) do
|
|
963
1010
|
local payload = redis.call('HGET', KEYS[4], operation_id)
|
|
964
1011
|
if payload then
|
|
@@ -1000,6 +1047,7 @@ for _, operation_id in ipairs(due) do
|
|
|
1000
1047
|
end
|
|
1001
1048
|
claim.leaseKey = lease_key
|
|
1002
1049
|
claim.leaseValue = lease_value
|
|
1050
|
+
claim.claimToken = claim_token
|
|
1003
1051
|
local encoded = cjson.encode(claim)
|
|
1004
1052
|
redis.call('HSET', KEYS[4], operation_id, encoded)
|
|
1005
1053
|
redis.call('ZADD', KEYS[5], now + lease_ms, operation_id)
|
|
@@ -1049,6 +1097,7 @@ if #entries == 0 then
|
|
|
1049
1097
|
end
|
|
1050
1098
|
local claim = {
|
|
1051
1099
|
operationId = proposed_id,
|
|
1100
|
+
claimToken = claim_token,
|
|
1052
1101
|
entries = entries,
|
|
1053
1102
|
attempt = 0,
|
|
1054
1103
|
expiresAt = now + retry_retention_ms,
|
|
@@ -1068,6 +1117,7 @@ const ACK_SCRIPT = `
|
|
|
1068
1117
|
local payload = redis.call('HGET', KEYS[4], ARGV[1])
|
|
1069
1118
|
if not payload then return 0 end
|
|
1070
1119
|
local claim = cjson.decode(payload)
|
|
1120
|
+
if claim.claimToken ~= ARGV[2] then return 0 end
|
|
1071
1121
|
if claim.leaseKey and claim.leaseKey ~= '' and
|
|
1072
1122
|
redis.call('GET', claim.leaseKey) ~= claim.leaseValue then return 0 end
|
|
1073
1123
|
for _, entry in ipairs(claim.entries) do
|
|
@@ -1082,13 +1132,25 @@ const RETRY_SCRIPT = `
|
|
|
1082
1132
|
local payload = redis.call('HGET', KEYS[4], ARGV[1])
|
|
1083
1133
|
if not payload then return 0 end
|
|
1084
1134
|
local claim = cjson.decode(payload)
|
|
1085
|
-
claim.
|
|
1135
|
+
if claim.claimToken ~= ARGV[2] then return 0 end
|
|
1136
|
+
claim.attempt = tonumber(ARGV[3])
|
|
1086
1137
|
redis.call('HSET', KEYS[4], ARGV[1], cjson.encode(claim))
|
|
1087
|
-
local retry_at = tonumber(ARGV[
|
|
1138
|
+
local retry_at = tonumber(ARGV[4])
|
|
1088
1139
|
if claim.expiresAt and retry_at > tonumber(claim.expiresAt) then retry_at = tonumber(claim.expiresAt) end
|
|
1089
1140
|
redis.call('ZADD', KEYS[5], retry_at, ARGV[1])
|
|
1090
1141
|
return 1
|
|
1091
1142
|
`;
|
|
1143
|
+
const RENEW_CLAIM_SCRIPT = `
|
|
1144
|
+
local payload = redis.call('HGET', KEYS[4], ARGV[1])
|
|
1145
|
+
if not payload then return 0 end
|
|
1146
|
+
local claim = cjson.decode(payload)
|
|
1147
|
+
if claim.claimToken ~= ARGV[2] then return 0 end
|
|
1148
|
+
if claim.leaseKey and claim.leaseKey ~= '' and
|
|
1149
|
+
redis.call('GET', claim.leaseKey) ~= claim.leaseValue then return 0 end
|
|
1150
|
+
if tonumber(redis.call('ZSCORE', KEYS[5], ARGV[1]) or 0) <= tonumber(ARGV[3]) then return 0 end
|
|
1151
|
+
redis.call('ZADD', KEYS[5], ARGV[4], ARGV[1])
|
|
1152
|
+
return 1
|
|
1153
|
+
`;
|
|
1092
1154
|
const HEALTH_SCRIPT = `
|
|
1093
1155
|
local oldest = redis.call('ZRANGE', KEYS[2], 0, 0, 'WITHSCORES')
|
|
1094
1156
|
return cjson.encode({
|
package/dist/oauth-module.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineModule, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
2
|
+
import { INTERNAL_MODULE_MANIFEST_FIELDS } from "./internal-module-manifest.js";
|
|
2
3
|
const MODULE_ID = "helyx.oauth";
|
|
3
4
|
const MODAL_ID = "helyx.oauth.revoke";
|
|
4
5
|
const SERVICE_FIELD_ID = "helyx.oauth.revoke.service";
|
|
@@ -15,7 +16,7 @@ const everyonePolicy = {
|
|
|
15
16
|
export function createOAuthModule() {
|
|
16
17
|
return defineModule({
|
|
17
18
|
manifest: {
|
|
18
|
-
|
|
19
|
+
...INTERNAL_MODULE_MANIFEST_FIELDS,
|
|
19
20
|
releaseStatus: "live",
|
|
20
21
|
id: MODULE_ID,
|
|
21
22
|
name: "Helyx OAuth",
|
|
@@ -43,6 +44,7 @@ export function createOAuthModule() {
|
|
|
43
44
|
{
|
|
44
45
|
name: "revoke",
|
|
45
46
|
description: "Revoke a service and delete the OAuth details Helyx stores for you.",
|
|
47
|
+
acknowledgement: "immediate-modal",
|
|
46
48
|
rateLimit: { scope: "user", limit: 5, windowSeconds: 60 },
|
|
47
49
|
execute: async (context) => {
|
|
48
50
|
const oauth = context.services.get(HELYX_SERVICE_NAMES.oauth);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { defineModule, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
2
|
+
import { isCustomerSelectableModule } from "@helyx/module-manifest";
|
|
2
3
|
import { PermissionFlagsBits } from "discord.js";
|
|
3
4
|
import { canMutatePermissionPolicy, permissionMutationDenialMessage, } from "@helyx/core";
|
|
5
|
+
import { INTERNAL_MODULE_MANIFEST_FIELDS } from "./internal-module-manifest.js";
|
|
4
6
|
const MODULE_SELECT_ID = "helyx.permissions.module";
|
|
5
7
|
const TARGET_SELECT_ID = "helyx.permissions.target";
|
|
6
8
|
const MODULE_PREVIOUS_ID = "helyx.permissions.module.previous";
|
|
@@ -17,13 +19,15 @@ const MODULE_PAGE_SIZE = 25;
|
|
|
17
19
|
const TARGET_PAGE_SIZE = 24;
|
|
18
20
|
const VIOLET = 0x7c3aed;
|
|
19
21
|
export function createPermissionsModule(configurableModules) {
|
|
20
|
-
const modules = new Map(configurableModules
|
|
22
|
+
const modules = new Map(configurableModules
|
|
23
|
+
.filter((module) => isCustomerSelectableModule(module.manifest))
|
|
24
|
+
.map((module) => [module.manifest.id, module]));
|
|
21
25
|
const commandModules = [...modules.values()]
|
|
22
26
|
.filter((module) => commandTargets(module).length > 0)
|
|
23
27
|
.sort((left, right) => left.manifest.name.localeCompare(right.manifest.name, "en-GB"));
|
|
24
28
|
return defineModule({
|
|
25
29
|
manifest: {
|
|
26
|
-
|
|
30
|
+
...INTERNAL_MODULE_MANIFEST_FIELDS,
|
|
27
31
|
releaseStatus: "live",
|
|
28
32
|
id: "helyx.permissions",
|
|
29
33
|
name: "Permissions",
|
|
@@ -46,6 +50,7 @@ export function createPermissionsModule(configurableModules) {
|
|
|
46
50
|
{
|
|
47
51
|
name: "permissions",
|
|
48
52
|
description: "Configure access to Helyx modules and commands",
|
|
53
|
+
acknowledgement: "deferred-ephemeral",
|
|
49
54
|
registration: "global",
|
|
50
55
|
contexts: ["guild"],
|
|
51
56
|
integrationTypes: ["guild_install"],
|
|
@@ -359,9 +364,17 @@ function restrictedPolicy() {
|
|
|
359
364
|
};
|
|
360
365
|
}
|
|
361
366
|
function commandTargets(module) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
367
|
+
const targets = (module.interactions?.commands ?? []).flatMap((command) => {
|
|
368
|
+
const nested = [
|
|
369
|
+
...(command.subcommands ?? []).map((subcommand) => `${command.name}.${subcommand.name}`),
|
|
370
|
+
...(command.subcommandGroups ?? []).flatMap((group) => group.subcommands.map((subcommand) => `${command.name}.${group.name}.${subcommand.name}`)),
|
|
371
|
+
];
|
|
372
|
+
if (nested.length > 0)
|
|
373
|
+
return nested;
|
|
374
|
+
return [command.name];
|
|
375
|
+
});
|
|
376
|
+
targets.push(...(module.nexusCommands ?? []).map((command) => `nexus.${command.path}`));
|
|
377
|
+
return [...new Set(targets)];
|
|
365
378
|
}
|
|
366
379
|
function modulePickerResponse(modules, requestedPage) {
|
|
367
380
|
const pages = pageCount(modules.length, MODULE_PAGE_SIZE);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface RenewableLeaseOptions {
|
|
2
|
+
intervalMs: number;
|
|
3
|
+
renew(): Promise<void>;
|
|
4
|
+
onError?(error: Error): void;
|
|
5
|
+
}
|
|
6
|
+
export declare class RenewableLease {
|
|
7
|
+
#private;
|
|
8
|
+
private readonly options;
|
|
9
|
+
constructor(options: RenewableLeaseOptions);
|
|
10
|
+
start(): void;
|
|
11
|
+
assertOwned(): void;
|
|
12
|
+
run<Result>(operation: (signal: AbortSignal) => Promise<Result>): Promise<Result>;
|
|
13
|
+
stop(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=renewable-lease.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export class RenewableLease {
|
|
2
|
+
options;
|
|
3
|
+
#controller = new AbortController();
|
|
4
|
+
#lost;
|
|
5
|
+
#resolveLost;
|
|
6
|
+
#failure;
|
|
7
|
+
#renewing;
|
|
8
|
+
#timer;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
let resolveLost;
|
|
12
|
+
this.#lost = new Promise((resolve) => {
|
|
13
|
+
resolveLost = resolve;
|
|
14
|
+
});
|
|
15
|
+
this.#resolveLost = resolveLost;
|
|
16
|
+
}
|
|
17
|
+
start() {
|
|
18
|
+
if (this.#timer || this.#failure)
|
|
19
|
+
return;
|
|
20
|
+
this.#timer = setInterval(() => this.#tick(), this.options.intervalMs);
|
|
21
|
+
this.#timer.unref();
|
|
22
|
+
}
|
|
23
|
+
assertOwned() {
|
|
24
|
+
if (this.#failure)
|
|
25
|
+
throw this.#failure;
|
|
26
|
+
}
|
|
27
|
+
async run(operation) {
|
|
28
|
+
this.assertOwned();
|
|
29
|
+
const operationOutcome = Promise.resolve()
|
|
30
|
+
.then(() => operation(this.#controller.signal))
|
|
31
|
+
.then((value) => ({ kind: "completed", value }), (error) => ({ kind: "failed", error }));
|
|
32
|
+
const outcome = await Promise.race([
|
|
33
|
+
operationOutcome,
|
|
34
|
+
this.#lost.then((error) => ({ kind: "lease-lost", error })),
|
|
35
|
+
]);
|
|
36
|
+
if (outcome.kind === "lease-lost") {
|
|
37
|
+
await operationOutcome;
|
|
38
|
+
throw outcome.error;
|
|
39
|
+
}
|
|
40
|
+
if (outcome.kind === "failed")
|
|
41
|
+
throw outcome.error;
|
|
42
|
+
this.assertOwned();
|
|
43
|
+
return outcome.value;
|
|
44
|
+
}
|
|
45
|
+
async stop() {
|
|
46
|
+
if (this.#timer)
|
|
47
|
+
clearInterval(this.#timer);
|
|
48
|
+
this.#timer = undefined;
|
|
49
|
+
await this.#renewing;
|
|
50
|
+
}
|
|
51
|
+
#tick() {
|
|
52
|
+
if (this.#renewing || this.#failure)
|
|
53
|
+
return;
|
|
54
|
+
const renewing = this.options.renew().catch((error) => {
|
|
55
|
+
const failure = error instanceof Error ? error : new Error("Lease renewal failed");
|
|
56
|
+
this.#failure = failure;
|
|
57
|
+
this.#controller.abort(failure);
|
|
58
|
+
this.#resolveLost(failure);
|
|
59
|
+
this.options.onError?.(failure);
|
|
60
|
+
});
|
|
61
|
+
this.#renewing = renewing.finally(() => {
|
|
62
|
+
this.#renewing = undefined;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=renewable-lease.js.map
|
|
@@ -58,7 +58,14 @@ export class ScheduledTaskRunner {
|
|
|
58
58
|
const active = this.#active;
|
|
59
59
|
if (!active)
|
|
60
60
|
return;
|
|
61
|
-
await Promise.race([
|
|
61
|
+
const drainedBeforeDeadline = await Promise.race([
|
|
62
|
+
active.then(() => true),
|
|
63
|
+
wait(this.#stopTimeoutMs).then(() => false),
|
|
64
|
+
]);
|
|
65
|
+
if (!drainedBeforeDeadline) {
|
|
66
|
+
this.#logger.warn({ event: "scheduler.stop_deadline_exceeded" }, "Scheduled task runner is still draining after its shutdown deadline");
|
|
67
|
+
await active;
|
|
68
|
+
}
|
|
62
69
|
}
|
|
63
70
|
async runOnce() {
|
|
64
71
|
if (this.#stopping)
|
|
@@ -321,6 +328,13 @@ export function createLocalScheduledTaskRoute(input) {
|
|
|
321
328
|
const module = modules.get(task.moduleId);
|
|
322
329
|
if (!module)
|
|
323
330
|
return deferred("module_unavailable", DEFAULT_POLL_INTERVAL_MS * 30);
|
|
331
|
+
if (typeof input.services.has === "function" &&
|
|
332
|
+
input.services.has(HELYX_SERVICE_NAMES.moduleAvailability)) {
|
|
333
|
+
const availability = input.services.get(HELYX_SERVICE_NAMES.moduleAvailability);
|
|
334
|
+
if (typeof availability?.isAvailable === "function" &&
|
|
335
|
+
!(await availability.isAvailable(task.moduleId)))
|
|
336
|
+
return deferred("module_globally_disabled", DEFAULT_POLL_INTERVAL_MS * 30);
|
|
337
|
+
}
|
|
324
338
|
if (task.guildId) {
|
|
325
339
|
const enabled = await input.services
|
|
326
340
|
.get(HELYX_SERVICE_NAMES.installations)
|
package/dist/shard-router.d.ts
CHANGED
|
@@ -11,11 +11,13 @@ export declare class ShardControlRouter implements BotControlHandler {
|
|
|
11
11
|
localLeases: ReadonlyMap<number, ShardLease>;
|
|
12
12
|
dispatchLocal: LocalDispatcher;
|
|
13
13
|
privacy?: Pick<BotControlHandler, "listPrivacyProviders" | "capturePrivacy" | "erasePrivacy">;
|
|
14
|
+
availability?: Pick<BotControlHandler, "listAvailabilityModules">;
|
|
14
15
|
localLeaderLease?: ShardLeaderLease;
|
|
15
16
|
coordinator?: ShardCoordinator;
|
|
16
17
|
notificationGuildId?: string;
|
|
17
18
|
});
|
|
18
19
|
cleanupOAuthConsumer(input: Parameters<BotControlHandler["cleanupOAuthConsumer"]>[0]): ReturnType<BotControlHandler["cleanupOAuthConsumer"]>;
|
|
20
|
+
listAvailabilityModules(): ReturnType<BotControlHandler["listAvailabilityModules"]>;
|
|
19
21
|
getGuildState(guildId: string): ReturnType<BotControlHandler["getGuildState"]>;
|
|
20
22
|
canAccessDashboard(input: Parameters<BotControlHandler["canAccessDashboard"]>[0]): ReturnType<BotControlHandler["canAccessDashboard"]>;
|
|
21
23
|
getGuildAccessPolicy(input: Parameters<BotControlHandler["getGuildAccessPolicy"]>[0]): ReturnType<BotControlHandler["getGuildAccessPolicy"]>;
|
package/dist/shard-router.js
CHANGED
|
@@ -15,6 +15,11 @@ export class ShardControlRouter {
|
|
|
15
15
|
cleanupOAuthConsumer(input) {
|
|
16
16
|
return this.#route({ kind: "oauth.consumer.cleanup", ...input });
|
|
17
17
|
}
|
|
18
|
+
listAvailabilityModules() {
|
|
19
|
+
if (!this.input.availability)
|
|
20
|
+
return Promise.reject(new BotControlTransportError(503, "Module availability catalogue is unavailable"));
|
|
21
|
+
return this.input.availability.listAvailabilityModules();
|
|
22
|
+
}
|
|
18
23
|
getGuildState(guildId) {
|
|
19
24
|
return this.#route({ kind: "guild.state.get", guildId });
|
|
20
25
|
}
|
|
@@ -9,6 +9,11 @@ export declare function supervisorShutdownExitCode(reason: string): number | und
|
|
|
9
9
|
export declare function createStartupWorkerHeartbeatMaintenance(heartbeat: () => void, intervalMs?: number): {
|
|
10
10
|
stop(): void;
|
|
11
11
|
};
|
|
12
|
+
export declare function relinquishRestartCircuitOwnership(input: {
|
|
13
|
+
shardId: number;
|
|
14
|
+
localLeases: Map<number, ShardLease>;
|
|
15
|
+
release(lease: ShardLease): Promise<boolean>;
|
|
16
|
+
}): Promise<boolean>;
|
|
12
17
|
export declare function deferScheduledRoutingFailures(route: () => Promise<ScheduledTaskExecutionResult>): Promise<ScheduledTaskExecutionResult>;
|
|
13
18
|
export declare function startShardSupervisor(environment: BotEnvironment, processEnvironment: NodeJS.ProcessEnv, workerEntry: string): Promise<BotHandle>;
|
|
14
19
|
export declare function splitNumericCommittedBatch(batch: NumericWriteBehindCommittedBatch, index: number): NumericWriteBehindCommittedBatch;
|