@frockbot/plugin-flock 0.3.19 → 0.3.21
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/package.json +7 -7
- package/src/client/index.test.ts +105 -0
- package/src/client/index.ts +31 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-flock",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.21",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -28,16 +28,16 @@
|
|
|
28
28
|
"typecheck": "vue-tsc --noEmit -p tsconfig.json"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@frockbot/client-core": "0.3.
|
|
32
|
-
"@frockbot/client-ui": "0.3.
|
|
33
|
-
"@frockbot/configuration-core": "0.3.
|
|
34
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
35
|
-
"@frockbot/plugin-shell": "0.3.
|
|
31
|
+
"@frockbot/client-core": "0.3.21",
|
|
32
|
+
"@frockbot/client-ui": "0.3.21",
|
|
33
|
+
"@frockbot/configuration-core": "0.3.21",
|
|
34
|
+
"@frockbot/kernel-contracts": "0.3.21",
|
|
35
|
+
"@frockbot/plugin-shell": "0.3.21",
|
|
36
36
|
"cordis": "4.0.0-rc.8",
|
|
37
37
|
"vue": "3.5.41"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@frockbot/plugin-testkit": "0.3.
|
|
40
|
+
"@frockbot/plugin-testkit": "0.3.21",
|
|
41
41
|
"@types/bun": "1.4.0",
|
|
42
42
|
"@vitejs/plugin-vue": "6.0.8",
|
|
43
43
|
"css-tree": "2.3.1",
|
package/src/client/index.test.ts
CHANGED
|
@@ -735,3 +735,108 @@ describe("Flock sidebar rows follow the transcript", () => {
|
|
|
735
735
|
});
|
|
736
736
|
});
|
|
737
737
|
});
|
|
738
|
+
|
|
739
|
+
describe("Flock list follows the account", () => {
|
|
740
|
+
test("a Bot deleted elsewhere moves this tab's selection on the next unread beat", async () => {
|
|
741
|
+
installStorage();
|
|
742
|
+
const location = { href: "https://app.example/?bot=alpha" };
|
|
743
|
+
Object.defineProperty(globalThis, "window", {
|
|
744
|
+
configurable: true,
|
|
745
|
+
value: {
|
|
746
|
+
location,
|
|
747
|
+
history: {
|
|
748
|
+
state: null,
|
|
749
|
+
replaceState: (_state: unknown, _title: string, url: URL) => {
|
|
750
|
+
location.href = url.href;
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
const sheep = randomSheepRecipeV1(() => 0);
|
|
756
|
+
// The directory this tab read yesterday still has alpha; the account has
|
|
757
|
+
// since deleted it (from the phone, say) and made gamma.
|
|
758
|
+
let bots = ["alpha", "beta"];
|
|
759
|
+
let directoryReads = 0;
|
|
760
|
+
const state = mount((path) => {
|
|
761
|
+
if (path === "/api/bots/identities")
|
|
762
|
+
return Promise.resolve({ schemaVersion: 1, identities: [] });
|
|
763
|
+
if (path === "/api/bots") {
|
|
764
|
+
directoryReads += 1;
|
|
765
|
+
return Promise.resolve({
|
|
766
|
+
schemaVersion: 1,
|
|
767
|
+
revision: directoryReads,
|
|
768
|
+
bots: bots.map((botId) => ({
|
|
769
|
+
schemaVersion: 1,
|
|
770
|
+
botId,
|
|
771
|
+
registeredAt: new Date(0).toISOString(),
|
|
772
|
+
initialName: botId,
|
|
773
|
+
sheep,
|
|
774
|
+
})),
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
if (path === "/api/bots/lifecycles")
|
|
778
|
+
return Promise.resolve({
|
|
779
|
+
schemaVersion: 1,
|
|
780
|
+
lifecycles: bots.map((botId) => ({
|
|
781
|
+
schemaVersion: 1,
|
|
782
|
+
botId,
|
|
783
|
+
status: "active",
|
|
784
|
+
revision: 0,
|
|
785
|
+
})),
|
|
786
|
+
});
|
|
787
|
+
if (path === "/api/bots/unread")
|
|
788
|
+
return Promise.resolve({
|
|
789
|
+
schemaVersion: 1,
|
|
790
|
+
unread: bots.map((botId) => ({
|
|
791
|
+
schemaVersion: 1,
|
|
792
|
+
botId,
|
|
793
|
+
count: 0,
|
|
794
|
+
capped: false,
|
|
795
|
+
unread: false,
|
|
796
|
+
manuallyUnread: false,
|
|
797
|
+
})),
|
|
798
|
+
});
|
|
799
|
+
if (path.endsWith("/sheep")) {
|
|
800
|
+
const botId = path.split("/")[3]!;
|
|
801
|
+
return Promise.resolve({ schemaVersion: 1, botId, revision: 0, sheep });
|
|
802
|
+
}
|
|
803
|
+
return Promise.reject(new Error(`unexpected request: ${path}`));
|
|
804
|
+
});
|
|
805
|
+
const selected: string[] = [];
|
|
806
|
+
const shell = ref({
|
|
807
|
+
activeBotId: undefined as string | undefined,
|
|
808
|
+
selectBot: (botId: string) => {
|
|
809
|
+
selected.push(botId);
|
|
810
|
+
shell.value.activeBotId = botId;
|
|
811
|
+
return Promise.resolve();
|
|
812
|
+
},
|
|
813
|
+
transcripts: {
|
|
814
|
+
rememberViewport: () => undefined,
|
|
815
|
+
viewportFor: () => undefined,
|
|
816
|
+
forget: () => undefined,
|
|
817
|
+
},
|
|
818
|
+
});
|
|
819
|
+
state.value.bindShell(shell as unknown as Ref<FrockBotWebData>);
|
|
820
|
+
await state.value.load();
|
|
821
|
+
expect(selected).toEqual(["alpha"]);
|
|
822
|
+
expect(directoryReads).toBe(1);
|
|
823
|
+
|
|
824
|
+
// A beat that agrees with the list on screen reads nothing again.
|
|
825
|
+
await state.value.refreshUnread();
|
|
826
|
+
expect(directoryReads).toBe(1);
|
|
827
|
+
|
|
828
|
+
bots = ["beta", "gamma"];
|
|
829
|
+
await state.value.refreshUnread();
|
|
830
|
+
expect(directoryReads).toBe(2);
|
|
831
|
+
expect(state.value.directory.bots.map((bot) => bot.botId)).toEqual([
|
|
832
|
+
"beta",
|
|
833
|
+
"gamma",
|
|
834
|
+
]);
|
|
835
|
+
expect(selected.at(-1)).toBe("beta");
|
|
836
|
+
expect(new URL(location.href).searchParams.get("bot")).toBe("beta");
|
|
837
|
+
|
|
838
|
+
// The same disagreement twice is still one reload.
|
|
839
|
+
await state.value.refreshUnread();
|
|
840
|
+
expect(directoryReads).toBe(2);
|
|
841
|
+
});
|
|
842
|
+
});
|
package/src/client/index.ts
CHANGED
|
@@ -122,6 +122,11 @@ export const flockClientPlugin: ClientPlugin = (ctx) => {
|
|
|
122
122
|
let unreadRefresh: Promise<void> | undefined;
|
|
123
123
|
/** A beat that arrived while one was in flight, replayed once it finishes. */
|
|
124
124
|
let unreadRefreshQueued = false;
|
|
125
|
+
/**
|
|
126
|
+
* The last flock the unread read disagreed with, so one disagreement is one
|
|
127
|
+
* reload and not a reload on every beat.
|
|
128
|
+
*/
|
|
129
|
+
let reconciledFlockSignature: string | undefined;
|
|
125
130
|
|
|
126
131
|
/**
|
|
127
132
|
* One unread read at a time, with at most one more behind it.
|
|
@@ -383,6 +388,32 @@ export const flockClientPlugin: ClientPlugin = (ctx) => {
|
|
|
383
388
|
const directory = decodeBotUnreadDirectoryViewV1(
|
|
384
389
|
await request("/api/bots/unread"),
|
|
385
390
|
);
|
|
391
|
+
// The unread read fans out over the whole non-archived flock, so it is
|
|
392
|
+
// also this tab's cheapest view of which Bots still exist. A Bot deleted
|
|
393
|
+
// or created from another tab or the phone leaves this tab's list
|
|
394
|
+
// stale, and a stale list can keep the conversation open on a Bot the
|
|
395
|
+
// server no longer has — every read of it answers 404 and the person
|
|
396
|
+
// cannot chat (2026-09-05: a day-old tab on a deleted Bob). A flock
|
|
397
|
+
// that differs from the list on screen is read again, which also moves
|
|
398
|
+
// the selection off a Bot that is gone. One disagreement is one reload.
|
|
399
|
+
if (state.value.loaded) {
|
|
400
|
+
const known = new Set(
|
|
401
|
+
state.value.directory.bots
|
|
402
|
+
.map((bot) => bot.botId)
|
|
403
|
+
.filter((botId) => state.value.lifecycles[botId] !== "archived"),
|
|
404
|
+
);
|
|
405
|
+
const seen = new Set(directory.unread.map((view) => view.botId));
|
|
406
|
+
const differs =
|
|
407
|
+
known.size !== seen.size ||
|
|
408
|
+
[...seen].some((botId) => !known.has(botId));
|
|
409
|
+
const signature = [...seen].sort().join("\n");
|
|
410
|
+
if (differs && signature !== reconciledFlockSignature) {
|
|
411
|
+
reconciledFlockSignature = signature;
|
|
412
|
+
await state.value.load();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (!differs) reconciledFlockSignature = undefined;
|
|
416
|
+
}
|
|
386
417
|
// A Turn that settles in the conversation the User is looking at has
|
|
387
418
|
// been read by the time it arrives, so the badge that counted it is
|
|
388
419
|
// wrong the instant it appears — and painting it for the beat before the
|