@frockbot/plugin-settings 0.1.3 → 0.1.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/frockbot.json +1 -0
- package/package.json +9 -9
- package/src/client/BotSettingsSurface.vue +22 -575
- package/src/client/ConnectionsSurface.vue +59 -10
- package/src/client/{PluginsTrigger.vue → ConnectorsTrigger.vue} +2 -2
- package/src/client/ModelsSurface.vue +26 -364
- package/src/client/PackageAccounts.vue +2 -25
- package/src/client/PackageCatalogSurface.vue +3 -3
- package/src/client/PackageSettingsForm.vue +6 -1
- package/src/client/PluginsSurface.vue +1 -1
- package/src/client/UserProfileTrigger.vue +2 -2
- package/src/client/UserSettingsSurface.vue +4 -18
- package/src/client/bot-settings.test.ts +71 -160
- package/src/client/bot-settings.ts +5 -42
- package/src/client/index.test.ts +7 -41
- package/src/client/index.ts +15 -5
- package/src/client/package-surfaces.test.ts +29 -3
- package/src/client/package-surfaces.ts +12 -3
- package/src/user.test.ts +270 -316
- package/src/user.ts +112 -344
- package/src/client/assignment-operations.ts +0 -22
package/src/user.ts
CHANGED
|
@@ -2,18 +2,18 @@ import {
|
|
|
2
2
|
configurationCommandFingerprintV1,
|
|
3
3
|
ConfigurationConflictError,
|
|
4
4
|
ConfigurationDecodeError,
|
|
5
|
+
decodePackageSettingIdsV1,
|
|
5
6
|
decodePackageSettingsPatchV1,
|
|
6
7
|
MAX_PACKAGE_SETTINGS_V1,
|
|
7
|
-
decodeConnectionDependencyRequirementV1,
|
|
8
8
|
decodeOperationReceiptV1,
|
|
9
9
|
decodeUserConfigurationExecuteRpcV1,
|
|
10
10
|
decodeUserConfigurationReadRpcV1,
|
|
11
11
|
decodeUserSettingsViewV1,
|
|
12
12
|
MAX_USER_CONNECTIONS_V1,
|
|
13
13
|
USER_PROFILE_PLACEHOLDER_NAME_V1,
|
|
14
|
-
type ConnectionDependencyRequirementV1,
|
|
15
14
|
type ConnectionView,
|
|
16
15
|
type JsonValue,
|
|
16
|
+
type PackageSettingValueV1,
|
|
17
17
|
type OperationReceiptV1,
|
|
18
18
|
type PackageInstallationView,
|
|
19
19
|
type UserConfigurationCommandV1,
|
|
@@ -41,16 +41,6 @@ const DEFAULT_PACKAGES_BOOTSTRAP_KEY = "user-default-packages-bootstrap:v1";
|
|
|
41
41
|
const CATALOG_PIN_KEY = "user-catalog-pin";
|
|
42
42
|
const IDENTITY_KEY = "user-id";
|
|
43
43
|
const RECEIPT_PREFIX = "configuration-receipt:";
|
|
44
|
-
const MAX_CONNECTION_DEPENDENCIES = 256;
|
|
45
|
-
|
|
46
|
-
type ConnectionDependency = {
|
|
47
|
-
botId: string;
|
|
48
|
-
generation: string;
|
|
49
|
-
packageId: string;
|
|
50
|
-
capabilityId: string;
|
|
51
|
-
claimOrder: number;
|
|
52
|
-
status: "pending" | "acknowledged";
|
|
53
|
-
};
|
|
54
44
|
|
|
55
45
|
interface StoredConfigurationReceipt {
|
|
56
46
|
commandFingerprint: string;
|
|
@@ -137,6 +127,10 @@ export interface AvailableUserPackage {
|
|
|
137
127
|
* from their first configuration read.
|
|
138
128
|
*/
|
|
139
129
|
installByDefault?: boolean;
|
|
130
|
+
/** The seeded installation state. Omission preserves the enabled default. */
|
|
131
|
+
defaultEnablement?: "enabled" | "disabled";
|
|
132
|
+
/** The Package manifest's Package-id-to-version-range dependency record. */
|
|
133
|
+
dependencies?: Readonly<Record<string, string>>;
|
|
140
134
|
/**
|
|
141
135
|
* `configuration.settings` from this version's manifest. Absent is the same
|
|
142
136
|
* as empty and means the Package offers no User-level setting, so every
|
|
@@ -195,61 +189,6 @@ function requireMatchingConfigurationReceipt(
|
|
|
195
189
|
return stored.receipt;
|
|
196
190
|
}
|
|
197
191
|
|
|
198
|
-
function connectionDependencies(
|
|
199
|
-
connection: ConnectionView,
|
|
200
|
-
): ConnectionDependency[] {
|
|
201
|
-
const value = connection.safeMetadata.dependentAssignments;
|
|
202
|
-
if (!Array.isArray(value)) return [];
|
|
203
|
-
return value.flatMap((candidate) => {
|
|
204
|
-
if (
|
|
205
|
-
!candidate ||
|
|
206
|
-
typeof candidate !== "object" ||
|
|
207
|
-
Array.isArray(candidate)
|
|
208
|
-
) {
|
|
209
|
-
return [];
|
|
210
|
-
}
|
|
211
|
-
const dependency = candidate as Record<string, unknown>;
|
|
212
|
-
if (
|
|
213
|
-
typeof dependency.botId !== "string" ||
|
|
214
|
-
typeof dependency.generation !== "string" ||
|
|
215
|
-
typeof dependency.packageId !== "string" ||
|
|
216
|
-
typeof dependency.capabilityId !== "string" ||
|
|
217
|
-
(dependency.claimOrder !== undefined &&
|
|
218
|
-
(!Number.isSafeInteger(dependency.claimOrder) ||
|
|
219
|
-
(dependency.claimOrder as number) < 0)) ||
|
|
220
|
-
(dependency.status !== "pending" && dependency.status !== "acknowledged")
|
|
221
|
-
) {
|
|
222
|
-
return [];
|
|
223
|
-
}
|
|
224
|
-
return [
|
|
225
|
-
{
|
|
226
|
-
botId: dependency.botId,
|
|
227
|
-
generation: dependency.generation,
|
|
228
|
-
packageId: dependency.packageId,
|
|
229
|
-
capabilityId: dependency.capabilityId,
|
|
230
|
-
claimOrder:
|
|
231
|
-
dependency.claimOrder === undefined
|
|
232
|
-
? 0
|
|
233
|
-
: (dependency.claimOrder as number),
|
|
234
|
-
status: dependency.status,
|
|
235
|
-
} satisfies ConnectionDependency,
|
|
236
|
-
];
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function withConnectionDependencies(
|
|
241
|
-
connection: ConnectionView,
|
|
242
|
-
dependencies: ConnectionDependency[],
|
|
243
|
-
): ConnectionView {
|
|
244
|
-
return {
|
|
245
|
-
...connection,
|
|
246
|
-
safeMetadata: {
|
|
247
|
-
...connection.safeMetadata,
|
|
248
|
-
dependentAssignments: dependencies,
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
192
|
/**
|
|
254
193
|
* An install may only name the generation this User is pinned to. Refusing
|
|
255
194
|
* anything else is what makes "Composition consumes immutable,
|
|
@@ -292,13 +231,17 @@ function withCatalogPin(
|
|
|
292
231
|
* client already reads needs no second source.
|
|
293
232
|
*/
|
|
294
233
|
function mergePackageSettingValues(
|
|
295
|
-
current: Record<string, JsonValue> | undefined,
|
|
296
|
-
patch: Record<string,
|
|
297
|
-
|
|
298
|
-
|
|
234
|
+
current: Record<string, JsonValue | PackageSettingValueV1> | undefined,
|
|
235
|
+
patch: Record<string, PackageSettingValueV1>,
|
|
236
|
+
unset: readonly string[],
|
|
237
|
+
): Record<string, JsonValue | PackageSettingValueV1> {
|
|
238
|
+
const merged: Record<string, JsonValue | PackageSettingValueV1> = {
|
|
239
|
+
...(current ?? {}),
|
|
240
|
+
};
|
|
299
241
|
for (const [settingId, value] of Object.entries(patch)) {
|
|
300
242
|
merged[settingId] = value;
|
|
301
243
|
}
|
|
244
|
+
for (const settingId of unset) delete merged[settingId];
|
|
302
245
|
if (Object.keys(merged).length > MAX_PACKAGE_SETTINGS_V1) {
|
|
303
246
|
throw new ConfigurationDecodeError("Package settings are too many");
|
|
304
247
|
}
|
|
@@ -317,12 +260,11 @@ function applyUserCommand(
|
|
|
317
260
|
switch (command.type) {
|
|
318
261
|
case "user/update-profile":
|
|
319
262
|
return { ...current, revision, profile: command.profile };
|
|
320
|
-
case "user/set-
|
|
263
|
+
case "user/set-platform-model":
|
|
321
264
|
return {
|
|
322
265
|
...current,
|
|
323
266
|
revision,
|
|
324
|
-
|
|
325
|
-
newBotModelTemplateSource: command.source,
|
|
267
|
+
platformModel: command.model,
|
|
326
268
|
};
|
|
327
269
|
case "user/install-package": {
|
|
328
270
|
const existing = current.packages.find(
|
|
@@ -346,7 +288,12 @@ function applyUserCommand(
|
|
|
346
288
|
{
|
|
347
289
|
packageId: command.packageId,
|
|
348
290
|
version: command.version,
|
|
349
|
-
state:
|
|
291
|
+
state:
|
|
292
|
+
existing?.state === "failed"
|
|
293
|
+
? "failed"
|
|
294
|
+
: command.enabled === false
|
|
295
|
+
? "disabled"
|
|
296
|
+
: "installed",
|
|
350
297
|
failure: existing?.failure,
|
|
351
298
|
// A Catalog install records where it came from; the compiled-in
|
|
352
299
|
// path records nothing new, so an old row keeps its exact shape.
|
|
@@ -363,10 +310,8 @@ function applyUserCommand(
|
|
|
363
310
|
};
|
|
364
311
|
}
|
|
365
312
|
case "user/uninstall-package": {
|
|
366
|
-
// Removing the row is the whole effect.
|
|
367
|
-
//
|
|
368
|
-
// unavailable tombstones the User can repair (ADR 0003), and
|
|
369
|
-
// Connections are the User's own and outlive any Package.
|
|
313
|
+
// Removing the row is the whole effect. Connections are the User's own
|
|
314
|
+
// and outlive any Package (ADR 0019).
|
|
370
315
|
if (
|
|
371
316
|
!current.packages.some((pkg) => pkg.packageId === command.packageId)
|
|
372
317
|
) {
|
|
@@ -391,20 +336,33 @@ function applyUserCommand(
|
|
|
391
336
|
}
|
|
392
337
|
// Validated against the manifest of the version this User has, not the
|
|
393
338
|
// one the client happened to be looking at.
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
339
|
+
const definitions = settingDefinitions(
|
|
340
|
+
installed.packageId,
|
|
341
|
+
installed.version,
|
|
397
342
|
);
|
|
343
|
+
const patch = command.values
|
|
344
|
+
? decodePackageSettingsPatchV1(definitions, command.values)
|
|
345
|
+
: {};
|
|
346
|
+
const unset = command.unset
|
|
347
|
+
? decodePackageSettingIdsV1(definitions, command.unset)
|
|
348
|
+
: [];
|
|
398
349
|
return {
|
|
399
350
|
...current,
|
|
400
351
|
revision,
|
|
401
352
|
packages: current.packages.map((pkg) =>
|
|
402
|
-
pkg.packageId
|
|
403
|
-
?
|
|
404
|
-
|
|
405
|
-
values
|
|
406
|
-
|
|
407
|
-
|
|
353
|
+
pkg.packageId !== command.packageId
|
|
354
|
+
? pkg
|
|
355
|
+
: (() => {
|
|
356
|
+
const values = mergePackageSettingValues(
|
|
357
|
+
pkg.values,
|
|
358
|
+
patch,
|
|
359
|
+
unset,
|
|
360
|
+
);
|
|
361
|
+
const { values: _storedValues, ...withoutValues } = pkg;
|
|
362
|
+
return Object.keys(values).length > 0
|
|
363
|
+
? { ...withoutValues, values }
|
|
364
|
+
: withoutValues;
|
|
365
|
+
})(),
|
|
408
366
|
),
|
|
409
367
|
};
|
|
410
368
|
}
|
|
@@ -435,6 +393,12 @@ function applyUserCommand(
|
|
|
435
393
|
export class UserSettingsBackendContribution {
|
|
436
394
|
private readonly availablePackages: ReadonlySet<string>;
|
|
437
395
|
|
|
396
|
+
/** Declared Package dependencies, by Package id and version. */
|
|
397
|
+
private readonly packageDependencies: ReadonlyMap<
|
|
398
|
+
string,
|
|
399
|
+
Readonly<Record<string, string>>
|
|
400
|
+
>;
|
|
401
|
+
|
|
438
402
|
/** The immutable first-party installation rows written on first read. */
|
|
439
403
|
private readonly defaultPackages: readonly PackageInstallationView[];
|
|
440
404
|
|
|
@@ -457,6 +421,12 @@ export class UserSettingsBackendContribution {
|
|
|
457
421
|
({ packageId, version }) => `${packageId}\u0000${version}`,
|
|
458
422
|
),
|
|
459
423
|
);
|
|
424
|
+
this.packageDependencies = new Map(
|
|
425
|
+
host.availablePackages.map((pkg) => [
|
|
426
|
+
`${pkg.packageId}\u0000${pkg.version}`,
|
|
427
|
+
pkg.dependencies ?? {},
|
|
428
|
+
]),
|
|
429
|
+
);
|
|
460
430
|
this.packageSettingDefinitions = new Map(
|
|
461
431
|
host.availablePackages.map((pkg) => [
|
|
462
432
|
`${pkg.packageId}\u0000${pkg.version}`,
|
|
@@ -469,7 +439,10 @@ export class UserSettingsBackendContribution {
|
|
|
469
439
|
{
|
|
470
440
|
packageId: pkg.packageId,
|
|
471
441
|
version: pkg.version,
|
|
472
|
-
state:
|
|
442
|
+
state:
|
|
443
|
+
pkg.defaultEnablement === "disabled"
|
|
444
|
+
? ("disabled" as const)
|
|
445
|
+
: ("installed" as const),
|
|
473
446
|
provenance: "first-party" as const,
|
|
474
447
|
},
|
|
475
448
|
]
|
|
@@ -549,6 +522,26 @@ export class UserSettingsBackendContribution {
|
|
|
549
522
|
);
|
|
550
523
|
}
|
|
551
524
|
|
|
525
|
+
private packageDependencyFailure(
|
|
526
|
+
packageId: string,
|
|
527
|
+
version: string,
|
|
528
|
+
settings: UserSettingsViewV1,
|
|
529
|
+
): string | undefined {
|
|
530
|
+
const dependencies = this.packageDependencies.get(
|
|
531
|
+
`${packageId}\u0000${version}`,
|
|
532
|
+
);
|
|
533
|
+
if (!dependencies) return undefined;
|
|
534
|
+
for (const dependencyId of Object.keys(dependencies).sort()) {
|
|
535
|
+
const available = settings.packages.some(
|
|
536
|
+
(pkg) => pkg.packageId === dependencyId && pkg.state === "installed",
|
|
537
|
+
);
|
|
538
|
+
if (!available) {
|
|
539
|
+
return `Package "${packageId}" requires Package "${dependencyId}" to be installed and enabled; enable "${dependencyId}" first`;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return undefined;
|
|
543
|
+
}
|
|
544
|
+
|
|
552
545
|
async readConfiguration(input: unknown): Promise<UserSettingsViewV1> {
|
|
553
546
|
const request = decodeUserConfigurationReadRpcV1(input);
|
|
554
547
|
for (const bootstrap of this.readBootstraps.values()) {
|
|
@@ -755,6 +748,36 @@ export class UserSettingsBackendContribution {
|
|
|
755
748
|
if (command.expectedRevision !== current.revision) {
|
|
756
749
|
throw new ConfigurationConflictError(current.revision);
|
|
757
750
|
}
|
|
751
|
+
let dependencyFailure: string | undefined;
|
|
752
|
+
if (command.type === "user/install-package" && command.enabled !== false) {
|
|
753
|
+
dependencyFailure = this.packageDependencyFailure(
|
|
754
|
+
command.packageId,
|
|
755
|
+
command.version,
|
|
756
|
+
current,
|
|
757
|
+
);
|
|
758
|
+
} else if (command.type === "user/set-package-enabled" && command.enabled) {
|
|
759
|
+
const installed = current.packages.find(
|
|
760
|
+
(pkg) => pkg.packageId === command.packageId,
|
|
761
|
+
);
|
|
762
|
+
if (installed) {
|
|
763
|
+
dependencyFailure = this.packageDependencyFailure(
|
|
764
|
+
installed.packageId,
|
|
765
|
+
installed.version,
|
|
766
|
+
current,
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (dependencyFailure) {
|
|
771
|
+
const receipt: OperationReceiptV1 = {
|
|
772
|
+
schemaVersion: 1,
|
|
773
|
+
commandId: command.commandId,
|
|
774
|
+
revision: current.revision,
|
|
775
|
+
status: "rejected",
|
|
776
|
+
failure: dependencyFailure,
|
|
777
|
+
};
|
|
778
|
+
await storage.put(receiptKey, { commandFingerprint, receipt });
|
|
779
|
+
return receipt;
|
|
780
|
+
}
|
|
758
781
|
const next = applyUserCommand(current, command, (packageId, version) =>
|
|
759
782
|
this.settingDefinitions(packageId, version),
|
|
760
783
|
);
|
|
@@ -937,261 +960,6 @@ export class UserSettingsBackendContribution {
|
|
|
937
960
|
);
|
|
938
961
|
}
|
|
939
962
|
|
|
940
|
-
/**
|
|
941
|
-
* The durable state of one Bot's dependency on one Connection. `absent` is
|
|
942
|
-
* the answer for a Connection this object does not hold, so a reconciling
|
|
943
|
-
* saga can distinguish "never claimed" from "claimed and pending".
|
|
944
|
-
*/
|
|
945
|
-
async readConnectionDependency(
|
|
946
|
-
userId: string,
|
|
947
|
-
connectionId: string,
|
|
948
|
-
botId: string,
|
|
949
|
-
generation: string,
|
|
950
|
-
): Promise<"absent" | "pending" | "acknowledged"> {
|
|
951
|
-
const connection = await this.getConnection(userId, connectionId);
|
|
952
|
-
if (!connection) return "absent";
|
|
953
|
-
const dependency = connectionDependencies(connection).find(
|
|
954
|
-
(candidate) =>
|
|
955
|
-
candidate.botId === botId && candidate.generation === generation,
|
|
956
|
-
);
|
|
957
|
-
return dependency?.status ?? "absent";
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
async claimConnectionDependency(
|
|
961
|
-
userId: string,
|
|
962
|
-
connectionId: string,
|
|
963
|
-
botId: string,
|
|
964
|
-
generation: string,
|
|
965
|
-
requirement: ConnectionDependencyRequirementV1,
|
|
966
|
-
storage?: UserSettingsTransaction,
|
|
967
|
-
): Promise<boolean> {
|
|
968
|
-
const decoded = decodeConnectionDependencyRequirementV1(requirement);
|
|
969
|
-
return this.transitionConnectionDependency(
|
|
970
|
-
userId,
|
|
971
|
-
connectionId,
|
|
972
|
-
(current, settings) => {
|
|
973
|
-
const installation = settings.packages.find(
|
|
974
|
-
(pkg) =>
|
|
975
|
-
pkg.packageId === decoded.packageId &&
|
|
976
|
-
pkg.version === decoded.packageVersion &&
|
|
977
|
-
pkg.state === "installed",
|
|
978
|
-
);
|
|
979
|
-
if (
|
|
980
|
-
!installation ||
|
|
981
|
-
current.state !== "ready" ||
|
|
982
|
-
current.packageId !== decoded.packageId ||
|
|
983
|
-
!decoded.connectionTypeIds.includes(current.connectionTypeId)
|
|
984
|
-
) {
|
|
985
|
-
return undefined;
|
|
986
|
-
}
|
|
987
|
-
const existing = connectionDependencies(current);
|
|
988
|
-
const replay = existing.find(
|
|
989
|
-
(dependency) =>
|
|
990
|
-
dependency.botId === botId && dependency.generation === generation,
|
|
991
|
-
);
|
|
992
|
-
if (replay) {
|
|
993
|
-
return replay.packageId === decoded.packageId &&
|
|
994
|
-
replay.capabilityId === decoded.capabilityId
|
|
995
|
-
? current
|
|
996
|
-
: undefined;
|
|
997
|
-
}
|
|
998
|
-
if (existing.length >= MAX_CONNECTION_DEPENDENCIES) return undefined;
|
|
999
|
-
return withConnectionDependencies(current, [
|
|
1000
|
-
...existing,
|
|
1001
|
-
{
|
|
1002
|
-
botId,
|
|
1003
|
-
generation,
|
|
1004
|
-
packageId: decoded.packageId,
|
|
1005
|
-
capabilityId: decoded.capabilityId,
|
|
1006
|
-
claimOrder: settings.revision + 1,
|
|
1007
|
-
status: "pending",
|
|
1008
|
-
},
|
|
1009
|
-
]);
|
|
1010
|
-
},
|
|
1011
|
-
storage,
|
|
1012
|
-
);
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
async acknowledgeConnectionDependency(
|
|
1016
|
-
userId: string,
|
|
1017
|
-
connectionId: string,
|
|
1018
|
-
botId: string,
|
|
1019
|
-
generation: string,
|
|
1020
|
-
): Promise<boolean> {
|
|
1021
|
-
return this.host.storage.transaction(async (storage) => {
|
|
1022
|
-
await this.assertIdentity(userId, storage);
|
|
1023
|
-
const current = await this.readSnapshot(storage);
|
|
1024
|
-
const target = current.connections.find(
|
|
1025
|
-
(connection) => connection.connectionId === connectionId,
|
|
1026
|
-
);
|
|
1027
|
-
if (
|
|
1028
|
-
!target ||
|
|
1029
|
-
target.state === "revoking" ||
|
|
1030
|
-
target.state === "revoked"
|
|
1031
|
-
) {
|
|
1032
|
-
return false;
|
|
1033
|
-
}
|
|
1034
|
-
const matched = connectionDependencies(target).find(
|
|
1035
|
-
(dependency) =>
|
|
1036
|
-
dependency.botId === botId && dependency.generation === generation,
|
|
1037
|
-
);
|
|
1038
|
-
if (!matched) return false;
|
|
1039
|
-
const latestClaimOrder = Math.max(
|
|
1040
|
-
...current.connections.flatMap((connection) =>
|
|
1041
|
-
connectionDependencies(connection).flatMap((dependency) =>
|
|
1042
|
-
dependency.botId === botId &&
|
|
1043
|
-
dependency.packageId === matched.packageId &&
|
|
1044
|
-
dependency.capabilityId === matched.capabilityId
|
|
1045
|
-
? [dependency.claimOrder]
|
|
1046
|
-
: [],
|
|
1047
|
-
),
|
|
1048
|
-
),
|
|
1049
|
-
);
|
|
1050
|
-
if (matched.claimOrder < latestClaimOrder) return false;
|
|
1051
|
-
if (
|
|
1052
|
-
matched.status === "acknowledged" &&
|
|
1053
|
-
!current.connections.some((connection) =>
|
|
1054
|
-
connectionDependencies(connection).some(
|
|
1055
|
-
(dependency) =>
|
|
1056
|
-
dependency.botId === botId &&
|
|
1057
|
-
dependency.packageId === matched.packageId &&
|
|
1058
|
-
dependency.capabilityId === matched.capabilityId &&
|
|
1059
|
-
(connection.connectionId !== connectionId ||
|
|
1060
|
-
dependency.generation !== generation),
|
|
1061
|
-
),
|
|
1062
|
-
)
|
|
1063
|
-
) {
|
|
1064
|
-
return true;
|
|
1065
|
-
}
|
|
1066
|
-
const connections = current.connections.map((connection) => {
|
|
1067
|
-
const dependencies = connectionDependencies(connection);
|
|
1068
|
-
const nextDependencies = dependencies.flatMap((dependency) => {
|
|
1069
|
-
const sameAuthority =
|
|
1070
|
-
dependency.botId === botId &&
|
|
1071
|
-
dependency.packageId === matched.packageId &&
|
|
1072
|
-
dependency.capabilityId === matched.capabilityId;
|
|
1073
|
-
if (!sameAuthority) return [dependency];
|
|
1074
|
-
if (
|
|
1075
|
-
connection.connectionId === connectionId &&
|
|
1076
|
-
dependency.generation === generation
|
|
1077
|
-
) {
|
|
1078
|
-
return [{ ...dependency, status: "acknowledged" as const }];
|
|
1079
|
-
}
|
|
1080
|
-
return [];
|
|
1081
|
-
});
|
|
1082
|
-
return nextDependencies.length === dependencies.length &&
|
|
1083
|
-
nextDependencies.every(
|
|
1084
|
-
(dependency, index) => dependency === dependencies[index],
|
|
1085
|
-
)
|
|
1086
|
-
? connection
|
|
1087
|
-
: withConnectionDependencies(connection, nextDependencies);
|
|
1088
|
-
});
|
|
1089
|
-
await storage.put(STATE_KEY, {
|
|
1090
|
-
...current,
|
|
1091
|
-
revision: current.revision + 1,
|
|
1092
|
-
connections,
|
|
1093
|
-
} satisfies UserSettingsViewV1);
|
|
1094
|
-
return true;
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
async releaseConnectionDependency(
|
|
1099
|
-
userId: string,
|
|
1100
|
-
connectionId: string,
|
|
1101
|
-
botId: string,
|
|
1102
|
-
generation: string,
|
|
1103
|
-
): Promise<boolean> {
|
|
1104
|
-
return this.host.storage.transaction(async (storage) => {
|
|
1105
|
-
await this.assertIdentity(userId, storage);
|
|
1106
|
-
const current = await this.readSnapshot(storage);
|
|
1107
|
-
const target = current.connections.find(
|
|
1108
|
-
(connection) => connection.connectionId === connectionId,
|
|
1109
|
-
);
|
|
1110
|
-
if (!target) return true;
|
|
1111
|
-
const existing = connectionDependencies(target);
|
|
1112
|
-
const matching = existing.filter(
|
|
1113
|
-
(dependency) =>
|
|
1114
|
-
dependency.botId === botId && dependency.generation === generation,
|
|
1115
|
-
);
|
|
1116
|
-
if (matching.length === 0) return true;
|
|
1117
|
-
if (matching.some((dependency) => dependency.status !== "acknowledged")) {
|
|
1118
|
-
return false;
|
|
1119
|
-
}
|
|
1120
|
-
const remaining = existing.filter(
|
|
1121
|
-
(dependency) =>
|
|
1122
|
-
dependency.botId !== botId || dependency.generation !== generation,
|
|
1123
|
-
);
|
|
1124
|
-
const connections = current.connections.map((connection) =>
|
|
1125
|
-
connection.connectionId === connectionId
|
|
1126
|
-
? withConnectionDependencies(connection, remaining)
|
|
1127
|
-
: connection,
|
|
1128
|
-
);
|
|
1129
|
-
await storage.put(STATE_KEY, {
|
|
1130
|
-
...current,
|
|
1131
|
-
revision: current.revision + 1,
|
|
1132
|
-
connections,
|
|
1133
|
-
} satisfies UserSettingsViewV1);
|
|
1134
|
-
return true;
|
|
1135
|
-
});
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
async compensateConnectionDependency(
|
|
1139
|
-
userId: string,
|
|
1140
|
-
connectionId: string,
|
|
1141
|
-
botId: string,
|
|
1142
|
-
generation: string,
|
|
1143
|
-
): Promise<boolean> {
|
|
1144
|
-
return this.transitionConnectionDependency(
|
|
1145
|
-
userId,
|
|
1146
|
-
connectionId,
|
|
1147
|
-
(current) => {
|
|
1148
|
-
const existing = connectionDependencies(current);
|
|
1149
|
-
const remaining = existing.filter(
|
|
1150
|
-
(dependency) =>
|
|
1151
|
-
dependency.botId !== botId ||
|
|
1152
|
-
dependency.generation !== generation ||
|
|
1153
|
-
dependency.status !== "pending",
|
|
1154
|
-
);
|
|
1155
|
-
return remaining.length === existing.length
|
|
1156
|
-
? undefined
|
|
1157
|
-
: withConnectionDependencies(current, remaining);
|
|
1158
|
-
},
|
|
1159
|
-
);
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
private async transitionConnectionDependency(
|
|
1163
|
-
userId: string,
|
|
1164
|
-
connectionId: string,
|
|
1165
|
-
transition: (
|
|
1166
|
-
connection: ConnectionView,
|
|
1167
|
-
settings: UserSettingsViewV1,
|
|
1168
|
-
) => ConnectionView | undefined,
|
|
1169
|
-
transaction?: UserSettingsTransaction,
|
|
1170
|
-
): Promise<boolean> {
|
|
1171
|
-
const apply = async (storage: UserSettingsTransaction) => {
|
|
1172
|
-
await this.assertIdentity(userId, storage);
|
|
1173
|
-
const current = await this.readSnapshot(storage);
|
|
1174
|
-
const connection = current.connections.find(
|
|
1175
|
-
(candidate) => candidate.connectionId === connectionId,
|
|
1176
|
-
);
|
|
1177
|
-
if (!connection) return false;
|
|
1178
|
-
const nextConnection = transition(connection, current);
|
|
1179
|
-
if (!nextConnection) return false;
|
|
1180
|
-
if (nextConnection === connection) return true;
|
|
1181
|
-
await storage.put(STATE_KEY, {
|
|
1182
|
-
...current,
|
|
1183
|
-
revision: current.revision + 1,
|
|
1184
|
-
connections: current.connections.map((candidate) =>
|
|
1185
|
-
candidate.connectionId === connectionId ? nextConnection : candidate,
|
|
1186
|
-
),
|
|
1187
|
-
} satisfies UserSettingsViewV1);
|
|
1188
|
-
return true;
|
|
1189
|
-
};
|
|
1190
|
-
return transaction
|
|
1191
|
-
? apply(transaction)
|
|
1192
|
-
: this.host.storage.transaction(apply);
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
963
|
private async assertIdentity(
|
|
1196
964
|
userId: string,
|
|
1197
965
|
storage: UserSettingsTransaction = this.host.storage,
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
BotSettingsViewV1,
|
|
3
|
-
CapabilityAssignmentOperationViewV1,
|
|
4
|
-
} from "@frockbot/configuration-core";
|
|
5
|
-
|
|
6
|
-
/** Projects every durable Assignment operation without requiring catalog state. */
|
|
7
|
-
export function projectAssignmentOperations(
|
|
8
|
-
settings: Pick<BotSettingsViewV1, "assignmentOperations"> | undefined,
|
|
9
|
-
): CapabilityAssignmentOperationViewV1[] {
|
|
10
|
-
return (settings?.assignmentOperations ?? []).map((operation) =>
|
|
11
|
-
structuredClone(operation),
|
|
12
|
-
);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function assignmentHasPendingOperation(
|
|
16
|
-
operations: readonly CapabilityAssignmentOperationViewV1[],
|
|
17
|
-
assignmentId: string,
|
|
18
|
-
): boolean {
|
|
19
|
-
return operations.some(
|
|
20
|
-
(operation) => operation.assignmentId === assignmentId,
|
|
21
|
-
);
|
|
22
|
-
}
|