@frockbot/plugin-settings 0.1.3 → 0.2.0
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 +425 -316
- package/src/user.ts +133 -346
- package/src/client/assignment-operations.ts +0 -22
package/src/user.ts
CHANGED
|
@@ -2,18 +2,19 @@ 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
|
+
migrateStoredUserSettingsV1,
|
|
12
13
|
MAX_USER_CONNECTIONS_V1,
|
|
13
14
|
USER_PROFILE_PLACEHOLDER_NAME_V1,
|
|
14
|
-
type ConnectionDependencyRequirementV1,
|
|
15
15
|
type ConnectionView,
|
|
16
16
|
type JsonValue,
|
|
17
|
+
type PackageSettingValueV1,
|
|
17
18
|
type OperationReceiptV1,
|
|
18
19
|
type PackageInstallationView,
|
|
19
20
|
type UserConfigurationCommandV1,
|
|
@@ -41,16 +42,6 @@ const DEFAULT_PACKAGES_BOOTSTRAP_KEY = "user-default-packages-bootstrap:v1";
|
|
|
41
42
|
const CATALOG_PIN_KEY = "user-catalog-pin";
|
|
42
43
|
const IDENTITY_KEY = "user-id";
|
|
43
44
|
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
45
|
|
|
55
46
|
interface StoredConfigurationReceipt {
|
|
56
47
|
commandFingerprint: string;
|
|
@@ -137,6 +128,10 @@ export interface AvailableUserPackage {
|
|
|
137
128
|
* from their first configuration read.
|
|
138
129
|
*/
|
|
139
130
|
installByDefault?: boolean;
|
|
131
|
+
/** The seeded installation state. Omission preserves the enabled default. */
|
|
132
|
+
defaultEnablement?: "enabled" | "disabled";
|
|
133
|
+
/** The Package manifest's Package-id-to-version-range dependency record. */
|
|
134
|
+
dependencies?: Readonly<Record<string, string>>;
|
|
140
135
|
/**
|
|
141
136
|
* `configuration.settings` from this version's manifest. Absent is the same
|
|
142
137
|
* as empty and means the Package offers no User-level setting, so every
|
|
@@ -195,61 +190,6 @@ function requireMatchingConfigurationReceipt(
|
|
|
195
190
|
return stored.receipt;
|
|
196
191
|
}
|
|
197
192
|
|
|
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
193
|
/**
|
|
254
194
|
* An install may only name the generation this User is pinned to. Refusing
|
|
255
195
|
* anything else is what makes "Composition consumes immutable,
|
|
@@ -292,13 +232,17 @@ function withCatalogPin(
|
|
|
292
232
|
* client already reads needs no second source.
|
|
293
233
|
*/
|
|
294
234
|
function mergePackageSettingValues(
|
|
295
|
-
current: Record<string, JsonValue> | undefined,
|
|
296
|
-
patch: Record<string,
|
|
297
|
-
|
|
298
|
-
|
|
235
|
+
current: Record<string, JsonValue | PackageSettingValueV1> | undefined,
|
|
236
|
+
patch: Record<string, PackageSettingValueV1>,
|
|
237
|
+
unset: readonly string[],
|
|
238
|
+
): Record<string, JsonValue | PackageSettingValueV1> {
|
|
239
|
+
const merged: Record<string, JsonValue | PackageSettingValueV1> = {
|
|
240
|
+
...(current ?? {}),
|
|
241
|
+
};
|
|
299
242
|
for (const [settingId, value] of Object.entries(patch)) {
|
|
300
243
|
merged[settingId] = value;
|
|
301
244
|
}
|
|
245
|
+
for (const settingId of unset) delete merged[settingId];
|
|
302
246
|
if (Object.keys(merged).length > MAX_PACKAGE_SETTINGS_V1) {
|
|
303
247
|
throw new ConfigurationDecodeError("Package settings are too many");
|
|
304
248
|
}
|
|
@@ -317,12 +261,11 @@ function applyUserCommand(
|
|
|
317
261
|
switch (command.type) {
|
|
318
262
|
case "user/update-profile":
|
|
319
263
|
return { ...current, revision, profile: command.profile };
|
|
320
|
-
case "user/set-
|
|
264
|
+
case "user/set-platform-model":
|
|
321
265
|
return {
|
|
322
266
|
...current,
|
|
323
267
|
revision,
|
|
324
|
-
|
|
325
|
-
newBotModelTemplateSource: command.source,
|
|
268
|
+
platformModel: command.model,
|
|
326
269
|
};
|
|
327
270
|
case "user/install-package": {
|
|
328
271
|
const existing = current.packages.find(
|
|
@@ -346,7 +289,12 @@ function applyUserCommand(
|
|
|
346
289
|
{
|
|
347
290
|
packageId: command.packageId,
|
|
348
291
|
version: command.version,
|
|
349
|
-
state:
|
|
292
|
+
state:
|
|
293
|
+
existing?.state === "failed"
|
|
294
|
+
? "failed"
|
|
295
|
+
: command.enabled === false
|
|
296
|
+
? "disabled"
|
|
297
|
+
: "installed",
|
|
350
298
|
failure: existing?.failure,
|
|
351
299
|
// A Catalog install records where it came from; the compiled-in
|
|
352
300
|
// path records nothing new, so an old row keeps its exact shape.
|
|
@@ -355,6 +303,9 @@ function applyUserCommand(
|
|
|
355
303
|
: {
|
|
356
304
|
catalogId: command.catalogId,
|
|
357
305
|
catalogGeneration: command.catalogGeneration,
|
|
306
|
+
...(command.contentHash === undefined
|
|
307
|
+
? {}
|
|
308
|
+
: { contentHash: command.contentHash }),
|
|
358
309
|
provenance: "catalog" as const,
|
|
359
310
|
}),
|
|
360
311
|
...(Object.keys(values).length === 0 ? {} : { values }),
|
|
@@ -363,10 +314,8 @@ function applyUserCommand(
|
|
|
363
314
|
};
|
|
364
315
|
}
|
|
365
316
|
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.
|
|
317
|
+
// Removing the row is the whole effect. Connections are the User's own
|
|
318
|
+
// and outlive any Package (ADR 0019).
|
|
370
319
|
if (
|
|
371
320
|
!current.packages.some((pkg) => pkg.packageId === command.packageId)
|
|
372
321
|
) {
|
|
@@ -391,20 +340,33 @@ function applyUserCommand(
|
|
|
391
340
|
}
|
|
392
341
|
// Validated against the manifest of the version this User has, not the
|
|
393
342
|
// one the client happened to be looking at.
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
343
|
+
const definitions = settingDefinitions(
|
|
344
|
+
installed.packageId,
|
|
345
|
+
installed.version,
|
|
397
346
|
);
|
|
347
|
+
const patch = command.values
|
|
348
|
+
? decodePackageSettingsPatchV1(definitions, command.values)
|
|
349
|
+
: {};
|
|
350
|
+
const unset = command.unset
|
|
351
|
+
? decodePackageSettingIdsV1(definitions, command.unset)
|
|
352
|
+
: [];
|
|
398
353
|
return {
|
|
399
354
|
...current,
|
|
400
355
|
revision,
|
|
401
356
|
packages: current.packages.map((pkg) =>
|
|
402
|
-
pkg.packageId
|
|
403
|
-
?
|
|
404
|
-
|
|
405
|
-
values
|
|
406
|
-
|
|
407
|
-
|
|
357
|
+
pkg.packageId !== command.packageId
|
|
358
|
+
? pkg
|
|
359
|
+
: (() => {
|
|
360
|
+
const values = mergePackageSettingValues(
|
|
361
|
+
pkg.values,
|
|
362
|
+
patch,
|
|
363
|
+
unset,
|
|
364
|
+
);
|
|
365
|
+
const { values: _storedValues, ...withoutValues } = pkg;
|
|
366
|
+
return Object.keys(values).length > 0
|
|
367
|
+
? { ...withoutValues, values }
|
|
368
|
+
: withoutValues;
|
|
369
|
+
})(),
|
|
408
370
|
),
|
|
409
371
|
};
|
|
410
372
|
}
|
|
@@ -435,6 +397,12 @@ function applyUserCommand(
|
|
|
435
397
|
export class UserSettingsBackendContribution {
|
|
436
398
|
private readonly availablePackages: ReadonlySet<string>;
|
|
437
399
|
|
|
400
|
+
/** Declared Package dependencies, by Package id and version. */
|
|
401
|
+
private readonly packageDependencies: ReadonlyMap<
|
|
402
|
+
string,
|
|
403
|
+
Readonly<Record<string, string>>
|
|
404
|
+
>;
|
|
405
|
+
|
|
438
406
|
/** The immutable first-party installation rows written on first read. */
|
|
439
407
|
private readonly defaultPackages: readonly PackageInstallationView[];
|
|
440
408
|
|
|
@@ -457,6 +425,12 @@ export class UserSettingsBackendContribution {
|
|
|
457
425
|
({ packageId, version }) => `${packageId}\u0000${version}`,
|
|
458
426
|
),
|
|
459
427
|
);
|
|
428
|
+
this.packageDependencies = new Map(
|
|
429
|
+
host.availablePackages.map((pkg) => [
|
|
430
|
+
`${pkg.packageId}\u0000${pkg.version}`,
|
|
431
|
+
pkg.dependencies ?? {},
|
|
432
|
+
]),
|
|
433
|
+
);
|
|
460
434
|
this.packageSettingDefinitions = new Map(
|
|
461
435
|
host.availablePackages.map((pkg) => [
|
|
462
436
|
`${pkg.packageId}\u0000${pkg.version}`,
|
|
@@ -469,7 +443,10 @@ export class UserSettingsBackendContribution {
|
|
|
469
443
|
{
|
|
470
444
|
packageId: pkg.packageId,
|
|
471
445
|
version: pkg.version,
|
|
472
|
-
state:
|
|
446
|
+
state:
|
|
447
|
+
pkg.defaultEnablement === "disabled"
|
|
448
|
+
? ("disabled" as const)
|
|
449
|
+
: ("installed" as const),
|
|
473
450
|
provenance: "first-party" as const,
|
|
474
451
|
},
|
|
475
452
|
]
|
|
@@ -549,6 +526,26 @@ export class UserSettingsBackendContribution {
|
|
|
549
526
|
);
|
|
550
527
|
}
|
|
551
528
|
|
|
529
|
+
private packageDependencyFailure(
|
|
530
|
+
packageId: string,
|
|
531
|
+
version: string,
|
|
532
|
+
settings: UserSettingsViewV1,
|
|
533
|
+
): string | undefined {
|
|
534
|
+
const dependencies = this.packageDependencies.get(
|
|
535
|
+
`${packageId}\u0000${version}`,
|
|
536
|
+
);
|
|
537
|
+
if (!dependencies) return undefined;
|
|
538
|
+
for (const dependencyId of Object.keys(dependencies).sort()) {
|
|
539
|
+
const available = settings.packages.some(
|
|
540
|
+
(pkg) => pkg.packageId === dependencyId && pkg.state === "installed",
|
|
541
|
+
);
|
|
542
|
+
if (!available) {
|
|
543
|
+
return `Package "${packageId}" requires Package "${dependencyId}" to be installed and enabled; enable "${dependencyId}" first`;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
|
|
552
549
|
async readConfiguration(input: unknown): Promise<UserSettingsViewV1> {
|
|
553
550
|
const request = decodeUserConfigurationReadRpcV1(input);
|
|
554
551
|
for (const bootstrap of this.readBootstraps.values()) {
|
|
@@ -622,6 +619,7 @@ export class UserSettingsBackendContribution {
|
|
|
622
619
|
version: string;
|
|
623
620
|
catalogId: string;
|
|
624
621
|
catalogGeneration: string;
|
|
622
|
+
contentHash?: string;
|
|
625
623
|
}): Promise<CatalogEntryV1> {
|
|
626
624
|
const catalog = this.host.catalog;
|
|
627
625
|
if (!catalog) {
|
|
@@ -646,6 +644,17 @@ export class UserSettingsBackendContribution {
|
|
|
646
644
|
`Catalog entry "${command.catalogId}" does not offer Package "${command.packageId}" at version "${command.version}"`,
|
|
647
645
|
);
|
|
648
646
|
}
|
|
647
|
+
if (entry.bundle) {
|
|
648
|
+
if (command.contentHash !== entry.bundle.contentHash) {
|
|
649
|
+
throw new Error(
|
|
650
|
+
`Catalog entry "${command.catalogId}" requires bundle hash "${entry.bundle.contentHash}"`,
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
} else if (command.contentHash !== undefined) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
`Catalog entry "${command.catalogId}" does not carry a Package bundle`,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
649
658
|
return entry;
|
|
650
659
|
}
|
|
651
660
|
|
|
@@ -663,6 +672,9 @@ export class UserSettingsBackendContribution {
|
|
|
663
672
|
version: command.version,
|
|
664
673
|
catalogId: command.catalogId,
|
|
665
674
|
catalogGeneration: command.catalogGeneration,
|
|
675
|
+
...(command.contentHash === undefined
|
|
676
|
+
? {}
|
|
677
|
+
: { contentHash: command.contentHash }),
|
|
666
678
|
})
|
|
667
679
|
: undefined;
|
|
668
680
|
return this.host.storage.transaction((storage) =>
|
|
@@ -738,7 +750,7 @@ export class UserSettingsBackendContribution {
|
|
|
738
750
|
const current =
|
|
739
751
|
storedSettings === undefined
|
|
740
752
|
? initialState()
|
|
741
|
-
: decodeUserSettingsViewV1(storedSettings);
|
|
753
|
+
: decodeUserSettingsViewV1(migrateStoredUserSettingsV1(storedSettings));
|
|
742
754
|
if (command.type === "user/set-package-enabled" && command.enabled) {
|
|
743
755
|
const installed = current.packages.find(
|
|
744
756
|
(pkg) => pkg.packageId === command.packageId,
|
|
@@ -755,6 +767,36 @@ export class UserSettingsBackendContribution {
|
|
|
755
767
|
if (command.expectedRevision !== current.revision) {
|
|
756
768
|
throw new ConfigurationConflictError(current.revision);
|
|
757
769
|
}
|
|
770
|
+
let dependencyFailure: string | undefined;
|
|
771
|
+
if (command.type === "user/install-package" && command.enabled !== false) {
|
|
772
|
+
dependencyFailure = this.packageDependencyFailure(
|
|
773
|
+
command.packageId,
|
|
774
|
+
command.version,
|
|
775
|
+
current,
|
|
776
|
+
);
|
|
777
|
+
} else if (command.type === "user/set-package-enabled" && command.enabled) {
|
|
778
|
+
const installed = current.packages.find(
|
|
779
|
+
(pkg) => pkg.packageId === command.packageId,
|
|
780
|
+
);
|
|
781
|
+
if (installed) {
|
|
782
|
+
dependencyFailure = this.packageDependencyFailure(
|
|
783
|
+
installed.packageId,
|
|
784
|
+
installed.version,
|
|
785
|
+
current,
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (dependencyFailure) {
|
|
790
|
+
const receipt: OperationReceiptV1 = {
|
|
791
|
+
schemaVersion: 1,
|
|
792
|
+
commandId: command.commandId,
|
|
793
|
+
revision: current.revision,
|
|
794
|
+
status: "rejected",
|
|
795
|
+
failure: dependencyFailure,
|
|
796
|
+
};
|
|
797
|
+
await storage.put(receiptKey, { commandFingerprint, receipt });
|
|
798
|
+
return receipt;
|
|
799
|
+
}
|
|
758
800
|
const next = applyUserCommand(current, command, (packageId, version) =>
|
|
759
801
|
this.settingDefinitions(packageId, version),
|
|
760
802
|
);
|
|
@@ -777,7 +819,7 @@ export class UserSettingsBackendContribution {
|
|
|
777
819
|
const stored = await storage.get<unknown>(STATE_KEY);
|
|
778
820
|
return stored === undefined
|
|
779
821
|
? initialState()
|
|
780
|
-
: decodeUserSettingsViewV1(stored);
|
|
822
|
+
: decodeUserSettingsViewV1(migrateStoredUserSettingsV1(stored));
|
|
781
823
|
}
|
|
782
824
|
|
|
783
825
|
async read(
|
|
@@ -937,261 +979,6 @@ export class UserSettingsBackendContribution {
|
|
|
937
979
|
);
|
|
938
980
|
}
|
|
939
981
|
|
|
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
982
|
private async assertIdentity(
|
|
1196
983
|
userId: string,
|
|
1197
984
|
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
|
-
}
|