pi-feats 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/README.md +22 -1
- package/THIRD_PARTY_NOTICES.md +12 -0
- package/extensions/api-server/application-log-store.ts +31 -0
- package/extensions/api-server/application-runtime.ts +80 -18
- package/extensions/api-server/application-store.ts +6 -2
- package/extensions/api-server/profile-store.ts +47 -20
- package/extensions/api-server/server.ts +182 -47
- package/extensions/browser-harness/cdp/commands.ts +260 -0
- package/extensions/browser-harness/cdp/console-buffer.ts +188 -0
- package/extensions/browser-harness/cdp/discovery.ts +198 -0
- package/extensions/browser-harness/cdp/errors.ts +23 -0
- package/extensions/browser-harness/cdp/events.ts +170 -0
- package/extensions/browser-harness/cdp/network-buffer.ts +146 -0
- package/extensions/browser-harness/cdp/ownership.ts +73 -0
- package/extensions/browser-harness/cdp/record-store.ts +61 -0
- package/extensions/browser-harness/cdp/session.ts +403 -0
- package/extensions/browser-harness/cdp/steel-transport.ts +151 -0
- package/extensions/browser-harness/cdp/target-factory.ts +153 -0
- package/extensions/browser-harness/cdp/types.ts +82 -0
- package/extensions/browser-harness/client.ts +366 -0
- package/extensions/browser-harness/daemon/bridge.ts +531 -0
- package/extensions/browser-harness/daemon/index.ts +86 -0
- package/extensions/browser-harness/daemon/protocol.ts +110 -0
- package/extensions/browser-harness/daemon/queue.ts +85 -0
- package/extensions/browser-harness/daemon/server.ts +170 -0
- package/extensions/browser-harness/daemon/spawn.ts +124 -0
- package/extensions/browser-harness/daemon/transport.ts +178 -0
- package/extensions/browser-harness/domains/ax-tree.ts +179 -0
- package/extensions/browser-harness/domains/box.ts +51 -0
- package/extensions/browser-harness/domains/cdp-call.ts +55 -0
- package/extensions/browser-harness/domains/click.ts +97 -0
- package/extensions/browser-harness/domains/console.ts +219 -0
- package/extensions/browser-harness/domains/dialog.ts +34 -0
- package/extensions/browser-harness/domains/drag.ts +67 -0
- package/extensions/browser-harness/domains/element-call.ts +117 -0
- package/extensions/browser-harness/domains/files.ts +219 -0
- package/extensions/browser-harness/domains/fill-engine.ts +80 -0
- package/extensions/browser-harness/domains/form-batch.ts +65 -0
- package/extensions/browser-harness/domains/form.ts +283 -0
- package/extensions/browser-harness/domains/headers.ts +89 -0
- package/extensions/browser-harness/domains/history.ts +80 -0
- package/extensions/browser-harness/domains/isolated-tab.ts +91 -0
- package/extensions/browser-harness/domains/js.ts +241 -0
- package/extensions/browser-harness/domains/keyboard.ts +197 -0
- package/extensions/browser-harness/domains/navigate.ts +161 -0
- package/extensions/browser-harness/domains/network.ts +301 -0
- package/extensions/browser-harness/domains/page.ts +154 -0
- package/extensions/browser-harness/domains/readpage/read-page.ts +174 -0
- package/extensions/browser-harness/domains/readpage/readability.ts +65 -0
- package/extensions/browser-harness/domains/ref-resolve.ts +118 -0
- package/extensions/browser-harness/domains/render.ts +43 -0
- package/extensions/browser-harness/domains/screenshot-capture.ts +80 -0
- package/extensions/browser-harness/domains/screenshot.ts +76 -0
- package/extensions/browser-harness/domains/scroll.ts +59 -0
- package/extensions/browser-harness/domains/search/google-serp.ts +85 -0
- package/extensions/browser-harness/domains/search/web-search.ts +144 -0
- package/extensions/browser-harness/domains/setup.ts +35 -0
- package/extensions/browser-harness/domains/snapshot.ts +256 -0
- package/extensions/browser-harness/domains/tabs.ts +202 -0
- package/extensions/browser-harness/domains/target.ts +35 -0
- package/extensions/browser-harness/domains/viewport.ts +42 -0
- package/extensions/browser-harness/index.ts +88 -0
- package/extensions/browser-harness/profile/bind.ts +90 -0
- package/extensions/browser-harness/profile/browser-process.ts +320 -0
- package/extensions/browser-harness/profile/command.ts +189 -0
- package/extensions/browser-harness/profile/launch.ts +80 -0
- package/extensions/browser-harness/profile/list.ts +131 -0
- package/extensions/browser-harness/profile/paths.ts +103 -0
- package/extensions/browser-harness/profile/store.ts +84 -0
- package/extensions/browser-harness/prompt.ts +16 -0
- package/extensions/browser-harness/registry.ts +80 -0
- package/extensions/browser-harness/schemas/common.ts +11 -0
- package/extensions/browser-harness/schemas/parse.ts +23 -0
- package/extensions/browser-harness/setup.ts +47 -0
- package/extensions/browser-harness/state.ts +51 -0
- package/extensions/browser-harness/steel.ts +215 -0
- package/extensions/browser-harness/util/debug.ts +8 -0
- package/extensions/browser-harness/util/guards.ts +30 -0
- package/extensions/browser-harness/util/js-template.ts +12 -0
- package/extensions/browser-harness/util/mutex.ts +20 -0
- package/extensions/browser-harness/util/paths.ts +31 -0
- package/extensions/browser-harness/util/result.ts +9 -0
- package/extensions/browser-harness/util/sharp-shim.ts +42 -0
- package/extensions/browser-harness/util/tool.ts +185 -0
- package/extensions/browser-harness/util/truncate.ts +41 -0
- package/extensions/cli-resources.ts +35 -16
- package/extensions/composio/index.ts +163 -0
- package/extensions/context-memory/index.ts +58 -0
- package/extensions/lib/application-context.ts +22 -0
- package/extensions/lib/application-handler-templates.ts +17 -1
- package/extensions/lib/context-memory.ts +165 -0
- package/extensions/lib/profile-sandbox.ts +3 -0
- package/extensions/lib/workflow-harness.ts +238 -0
- package/extensions/pi-console-webui/app/[section]/page.tsx +6 -1
- package/extensions/pi-console-webui/app/applications/[slug]/page.tsx +15 -0
- package/extensions/pi-console-webui/app/layout.tsx +6 -1
- package/extensions/pi-console-webui/app/login/page.tsx +5 -9
- package/extensions/pi-console-webui/app/not-found.tsx +5 -0
- package/extensions/pi-console-webui/app/page.tsx +6 -0
- package/extensions/pi-console-webui/app/terminal/page.tsx +3 -0
- package/extensions/pi-console-webui/components/application-handler-editor.tsx +4 -4
- package/extensions/pi-console-webui/components/application-logs.tsx +12 -8
- package/extensions/pi-console-webui/components/application-settings.tsx +5 -1
- package/extensions/pi-console-webui/components/chat-workspace.tsx +7 -4
- package/extensions/pi-console-webui/components/console-page.tsx +3 -1
- package/extensions/pi-console-webui/components/console.tsx +53 -7
- package/extensions/pi-console-webui/components/login-form.tsx +14 -0
- package/extensions/pi-console-webui/components/pulse-resources.tsx +10 -6
- package/extensions/pi-console-webui/components/skill-resources.tsx +3 -3
- package/extensions/pi-console-webui/lib/page-metadata.ts +33 -0
- package/extensions/profiles.ts +18 -7
- package/extensions/pulse/store.ts +5 -2
- package/extensions/sequential-workflow.ts +340 -256
- package/extensions/skill-sources/store.ts +24 -3
- package/licenses/pi-browser-harness-MIT.txt +21 -0
- package/package.json +21 -3
- package/schemas/sequential-workflow-template.schema.json +62 -0
- package/skills/browser-harness/SKILL.md +33 -0
- package/skills/composio-connections/SKILL.md +75 -0
- package/skills/sequential-workflow/SKILL.md +70 -0
- package/skills/sequential-workflow-template/SKILL.md +44 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Type, type Static, type TSchema } from "typebox";
|
|
2
|
+
import { Compile } from "typebox/compile";
|
|
3
|
+
import { type Result, err, ok } from "../util/result";
|
|
4
|
+
import { type CdpError, cdpError } from "./errors";
|
|
5
|
+
|
|
6
|
+
const ev = <P extends TSchema>(params: P) => ({ params, validate: Compile(params) });
|
|
7
|
+
|
|
8
|
+
const open = { additionalProperties: true } as const;
|
|
9
|
+
|
|
10
|
+
const Anything = Type.Object({}, open);
|
|
11
|
+
|
|
12
|
+
const StackTrace = Type.Object(
|
|
13
|
+
{
|
|
14
|
+
callFrames: Type.Optional(
|
|
15
|
+
Type.Array(
|
|
16
|
+
Type.Object(
|
|
17
|
+
{
|
|
18
|
+
url: Type.Optional(Type.String()),
|
|
19
|
+
lineNumber: Type.Optional(Type.Number()),
|
|
20
|
+
functionName: Type.Optional(Type.String()),
|
|
21
|
+
},
|
|
22
|
+
open,
|
|
23
|
+
),
|
|
24
|
+
),
|
|
25
|
+
),
|
|
26
|
+
},
|
|
27
|
+
open,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const ConsoleArg = Type.Object(
|
|
31
|
+
{
|
|
32
|
+
type: Type.Optional(Type.String()),
|
|
33
|
+
value: Type.Optional(Type.Unknown()),
|
|
34
|
+
description: Type.Optional(Type.String()),
|
|
35
|
+
unserializableValue: Type.Optional(Type.String()),
|
|
36
|
+
},
|
|
37
|
+
open,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
export const EVENTS = {
|
|
41
|
+
"Inspector.detached": ev(Anything),
|
|
42
|
+
|
|
43
|
+
"Page.javascriptDialogOpening": ev(
|
|
44
|
+
Type.Object(
|
|
45
|
+
{
|
|
46
|
+
type: Type.String(),
|
|
47
|
+
message: Type.String(),
|
|
48
|
+
defaultPrompt: Type.Optional(Type.String()),
|
|
49
|
+
},
|
|
50
|
+
open,
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
"Page.loadEventFired": ev(Anything),
|
|
54
|
+
"Page.frameNavigated": ev(Anything),
|
|
55
|
+
|
|
56
|
+
"Runtime.consoleAPICalled": ev(
|
|
57
|
+
Type.Object(
|
|
58
|
+
{
|
|
59
|
+
type: Type.Optional(Type.String()),
|
|
60
|
+
args: Type.Optional(Type.Array(ConsoleArg)),
|
|
61
|
+
stackTrace: Type.Optional(StackTrace),
|
|
62
|
+
},
|
|
63
|
+
open,
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
|
|
67
|
+
"Log.entryAdded": ev(
|
|
68
|
+
Type.Object(
|
|
69
|
+
{
|
|
70
|
+
entry: Type.Object(
|
|
71
|
+
{
|
|
72
|
+
level: Type.Optional(Type.String()),
|
|
73
|
+
text: Type.Optional(Type.String()),
|
|
74
|
+
url: Type.Optional(Type.String()),
|
|
75
|
+
lineNumber: Type.Optional(Type.Number()),
|
|
76
|
+
stackTrace: Type.Optional(StackTrace),
|
|
77
|
+
},
|
|
78
|
+
open,
|
|
79
|
+
),
|
|
80
|
+
},
|
|
81
|
+
open,
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
|
|
85
|
+
"Network.requestWillBeSent": ev(
|
|
86
|
+
Type.Object(
|
|
87
|
+
{
|
|
88
|
+
requestId: Type.String(),
|
|
89
|
+
request: Type.Object(
|
|
90
|
+
{
|
|
91
|
+
url: Type.String(),
|
|
92
|
+
method: Type.String(),
|
|
93
|
+
postData: Type.Optional(Type.String()),
|
|
94
|
+
},
|
|
95
|
+
open,
|
|
96
|
+
),
|
|
97
|
+
type: Type.Optional(Type.String()),
|
|
98
|
+
},
|
|
99
|
+
open,
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
"Network.responseReceived": ev(
|
|
103
|
+
Type.Object(
|
|
104
|
+
{
|
|
105
|
+
requestId: Type.String(),
|
|
106
|
+
response: Type.Optional(
|
|
107
|
+
Type.Object(
|
|
108
|
+
{
|
|
109
|
+
status: Type.Optional(Type.Number()),
|
|
110
|
+
statusText: Type.Optional(Type.String()),
|
|
111
|
+
mimeType: Type.Optional(Type.String()),
|
|
112
|
+
},
|
|
113
|
+
open,
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
type: Type.Optional(Type.String()),
|
|
117
|
+
},
|
|
118
|
+
open,
|
|
119
|
+
),
|
|
120
|
+
),
|
|
121
|
+
"Network.loadingFinished": ev(
|
|
122
|
+
Type.Object(
|
|
123
|
+
{ requestId: Type.String(), encodedDataLength: Type.Optional(Type.Number()) },
|
|
124
|
+
open,
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
"Network.loadingFailed": ev(
|
|
128
|
+
Type.Object(
|
|
129
|
+
{ requestId: Type.String(), errorText: Type.Optional(Type.String()) },
|
|
130
|
+
open,
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
|
|
134
|
+
"Target.targetCreated": ev(
|
|
135
|
+
Type.Object(
|
|
136
|
+
{
|
|
137
|
+
targetInfo: Type.Object(
|
|
138
|
+
{
|
|
139
|
+
targetId: Type.String(),
|
|
140
|
+
type: Type.String(),
|
|
141
|
+
openerId: Type.Optional(Type.String()),
|
|
142
|
+
},
|
|
143
|
+
open,
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
open,
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
"Target.targetDestroyed": ev(Type.Object({ targetId: Type.String() }, open)),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export type CdpEventName = keyof typeof EVENTS;
|
|
153
|
+
export type EventParamsOf<E extends CdpEventName> = Static<(typeof EVENTS)[E]["params"]>;
|
|
154
|
+
|
|
155
|
+
export const isKnownEvent = (name: string): name is CdpEventName => Object.hasOwn(EVENTS, name);
|
|
156
|
+
|
|
157
|
+
export const decodeEvent = <E extends CdpEventName>(
|
|
158
|
+
name: E,
|
|
159
|
+
raw: unknown,
|
|
160
|
+
): Result<EventParamsOf<E>, CdpError> => {
|
|
161
|
+
const spec = EVENTS[name];
|
|
162
|
+
if (spec === undefined) {
|
|
163
|
+
return err(cdpError("invalid_response", "unknown CDP event", name));
|
|
164
|
+
}
|
|
165
|
+
if (!spec.validate.Check(raw)) {
|
|
166
|
+
const why = spec.validate.Errors(raw).map((e) => e.message).join("; ");
|
|
167
|
+
return err(cdpError("invalid_response", why, name));
|
|
168
|
+
}
|
|
169
|
+
return ok(raw as EventParamsOf<E>);
|
|
170
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { decodeEvent, type EventParamsOf } from "./events";
|
|
2
|
+
import { createRecordStore } from "./record-store";
|
|
3
|
+
|
|
4
|
+
export type NetworkRecord = {
|
|
5
|
+
requestId: string;
|
|
6
|
+
method: string;
|
|
7
|
+
url: string;
|
|
8
|
+
status?: number;
|
|
9
|
+
statusText?: string;
|
|
10
|
+
type: string;
|
|
11
|
+
mimeType?: string;
|
|
12
|
+
requestStartedMs: number;
|
|
13
|
+
durationMs?: number;
|
|
14
|
+
requestBodySize: number;
|
|
15
|
+
responseBodySize?: number;
|
|
16
|
+
failed?: boolean;
|
|
17
|
+
errorText?: string;
|
|
18
|
+
body?: string | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type NetworkFilter = {
|
|
22
|
+
urlPattern?: string;
|
|
23
|
+
methodFilter?: string[];
|
|
24
|
+
statusFilter?: { min?: number; max?: number };
|
|
25
|
+
resourceTypes?: string[];
|
|
26
|
+
sinceMs?: number;
|
|
27
|
+
limit?: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type DrainResult = {
|
|
31
|
+
readonly records: NetworkRecord[];
|
|
32
|
+
readonly total: number;
|
|
33
|
+
readonly bufferOverflowed: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type NetworkBuffer = {
|
|
37
|
+
ingestRequestWillBeSent(p: unknown): void;
|
|
38
|
+
ingestResponseReceived(p: unknown): void;
|
|
39
|
+
ingestLoadingFinished(p: unknown): void;
|
|
40
|
+
ingestLoadingFailed(p: unknown): void;
|
|
41
|
+
drain(filter: NetworkFilter): DrainResult;
|
|
42
|
+
clear(): void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const compileUrlMatcher = (pattern: string): ((url: string) => boolean) => {
|
|
46
|
+
if (pattern.length >= 2 && pattern.startsWith("/") && pattern.endsWith("/")) {
|
|
47
|
+
try {
|
|
48
|
+
const re = new RegExp(pattern.slice(1, -1));
|
|
49
|
+
return (url) => re.test(url);
|
|
50
|
+
} catch {}
|
|
51
|
+
}
|
|
52
|
+
return (url) => url.includes(pattern);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type UpdateEvent = "Network.responseReceived" | "Network.loadingFinished" | "Network.loadingFailed";
|
|
56
|
+
|
|
57
|
+
export const createNetworkBuffer = (capacity = 500): NetworkBuffer => {
|
|
58
|
+
const records = createRecordStore<string, NetworkRecord>(capacity);
|
|
59
|
+
|
|
60
|
+
// All three follow-up events say the same thing first: decode, find the request they belong to, give up quietly if it is gone.
|
|
61
|
+
const update = <E extends UpdateEvent>(
|
|
62
|
+
method: E,
|
|
63
|
+
p: unknown,
|
|
64
|
+
apply: (r: NetworkRecord, params: EventParamsOf<E>) => void,
|
|
65
|
+
): void => {
|
|
66
|
+
const decoded = decodeEvent(method, p);
|
|
67
|
+
if (!decoded.success) return;
|
|
68
|
+
const id = decoded.data.requestId;
|
|
69
|
+
if (!id) return;
|
|
70
|
+
const r = records.get(id);
|
|
71
|
+
if (!r) return;
|
|
72
|
+
apply(r, decoded.data);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
ingestRequestWillBeSent(p) {
|
|
77
|
+
const decoded = decodeEvent("Network.requestWillBeSent", p);
|
|
78
|
+
const params = decoded.success ? decoded.data : undefined;
|
|
79
|
+
const id = params?.requestId;
|
|
80
|
+
const url = params?.request.url;
|
|
81
|
+
const method = params?.request.method;
|
|
82
|
+
if (!id || !url || !method) return;
|
|
83
|
+
records.delete(id);
|
|
84
|
+
const postData = params.request.postData;
|
|
85
|
+
records.set(id, {
|
|
86
|
+
requestId: id,
|
|
87
|
+
method,
|
|
88
|
+
url,
|
|
89
|
+
type: params.type ?? "Other",
|
|
90
|
+
requestStartedMs: Date.now(),
|
|
91
|
+
requestBodySize: postData ? Buffer.byteLength(postData, "utf8") : 0,
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
ingestResponseReceived(p) {
|
|
96
|
+
update("Network.responseReceived", p, (r, params) => {
|
|
97
|
+
if (params.response?.status !== undefined) r.status = params.response.status;
|
|
98
|
+
if (params.response?.statusText !== undefined && params.response.statusText !== "") r.statusText = params.response.statusText;
|
|
99
|
+
if (params.response?.mimeType !== undefined) r.mimeType = params.response.mimeType;
|
|
100
|
+
// type is more accurate on responseReceived (CDP sometimes refines it)
|
|
101
|
+
if (params.type !== undefined) r.type = params.type;
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
ingestLoadingFinished(p) {
|
|
106
|
+
update("Network.loadingFinished", p, (r, params) => {
|
|
107
|
+
if (params.encodedDataLength !== undefined) r.responseBodySize = params.encodedDataLength;
|
|
108
|
+
r.durationMs = Date.now() - r.requestStartedMs;
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
ingestLoadingFailed(p) {
|
|
113
|
+
update("Network.loadingFailed", p, (r, params) => {
|
|
114
|
+
r.failed = true;
|
|
115
|
+
if (params.errorText !== undefined) r.errorText = params.errorText;
|
|
116
|
+
r.durationMs = Date.now() - r.requestStartedMs;
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
drain(filter) {
|
|
121
|
+
const matchUrl = filter.urlPattern !== undefined ? compileUrlMatcher(filter.urlPattern) : undefined;
|
|
122
|
+
const methods = filter.methodFilter ? new Set(filter.methodFilter.map((m) => m.toUpperCase())) : undefined;
|
|
123
|
+
const types = filter.resourceTypes ? new Set(filter.resourceTypes.map((t) => t.toLowerCase())) : undefined;
|
|
124
|
+
const minStatus = filter.statusFilter?.min;
|
|
125
|
+
const maxStatus = filter.statusFilter?.max;
|
|
126
|
+
const cutoff = filter.sinceMs !== undefined ? Date.now() - filter.sinceMs : undefined;
|
|
127
|
+
|
|
128
|
+
const matched: NetworkRecord[] = [];
|
|
129
|
+
for (const r of records.values()) {
|
|
130
|
+
if (matchUrl && !matchUrl(r.url)) continue;
|
|
131
|
+
if (methods && !methods.has(r.method.toUpperCase())) continue;
|
|
132
|
+
if (types && !types.has(r.type.toLowerCase())) continue;
|
|
133
|
+
if (cutoff !== undefined && r.requestStartedMs < cutoff) continue;
|
|
134
|
+
if (minStatus !== undefined && (r.status === undefined || r.status < minStatus)) continue;
|
|
135
|
+
if (maxStatus !== undefined && (r.status === undefined || r.status > maxStatus)) continue;
|
|
136
|
+
matched.push({ ...r });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return records.page(matched, filter.limit);
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
clear() {
|
|
143
|
+
records.clear();
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type OwnershipRegistry = {
|
|
2
|
+
add(targetId: string): void;
|
|
3
|
+
remove(targetId: string): void;
|
|
4
|
+
has(targetId: string): boolean;
|
|
5
|
+
list(): ReadonlyArray<string>;
|
|
6
|
+
replaceAll(ids: ReadonlyArray<string>): void;
|
|
7
|
+
setHarnessWindow(targetId: string | undefined): void;
|
|
8
|
+
harnessWindow(): string | undefined;
|
|
9
|
+
setHarnessWindowId(windowId: number | undefined): void;
|
|
10
|
+
harnessWindowId(): number | undefined;
|
|
11
|
+
onChange(cb: () => void): void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const createOwnershipRegistry = (
|
|
15
|
+
initial?: {
|
|
16
|
+
readonly ownedTargetIds?: ReadonlyArray<string>;
|
|
17
|
+
readonly harnessWindowTargetId?: string;
|
|
18
|
+
readonly harnessWindowId?: number;
|
|
19
|
+
},
|
|
20
|
+
): OwnershipRegistry => {
|
|
21
|
+
const owned = new Set<string>(initial?.ownedTargetIds ?? []);
|
|
22
|
+
let harnessWindow: string | undefined = initial?.harnessWindowTargetId;
|
|
23
|
+
let harnessWindowId: number | undefined = initial?.harnessWindowId;
|
|
24
|
+
let listener: (() => void) | null = null;
|
|
25
|
+
|
|
26
|
+
const notify = (): void => {
|
|
27
|
+
if (listener) listener();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
add(targetId) {
|
|
32
|
+
if (owned.has(targetId)) return;
|
|
33
|
+
owned.add(targetId);
|
|
34
|
+
notify();
|
|
35
|
+
},
|
|
36
|
+
remove(targetId) {
|
|
37
|
+
if (!owned.has(targetId) && harnessWindow !== targetId) return;
|
|
38
|
+
owned.delete(targetId);
|
|
39
|
+
if (harnessWindow === targetId) harnessWindow = undefined;
|
|
40
|
+
notify();
|
|
41
|
+
},
|
|
42
|
+
has(targetId) {
|
|
43
|
+
return owned.has(targetId);
|
|
44
|
+
},
|
|
45
|
+
list() {
|
|
46
|
+
return [...owned];
|
|
47
|
+
},
|
|
48
|
+
replaceAll(ids) {
|
|
49
|
+
owned.clear();
|
|
50
|
+
for (const id of ids) owned.add(id);
|
|
51
|
+
notify();
|
|
52
|
+
},
|
|
53
|
+
setHarnessWindow(targetId) {
|
|
54
|
+
if (harnessWindow === targetId) return;
|
|
55
|
+
harnessWindow = targetId;
|
|
56
|
+
notify();
|
|
57
|
+
},
|
|
58
|
+
harnessWindow() {
|
|
59
|
+
return harnessWindow;
|
|
60
|
+
},
|
|
61
|
+
setHarnessWindowId(windowId) {
|
|
62
|
+
if (harnessWindowId === windowId) return;
|
|
63
|
+
harnessWindowId = windowId;
|
|
64
|
+
notify();
|
|
65
|
+
},
|
|
66
|
+
harnessWindowId() {
|
|
67
|
+
return harnessWindowId;
|
|
68
|
+
},
|
|
69
|
+
onChange(cb) {
|
|
70
|
+
listener = cb;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type DrainPage<T> = {
|
|
2
|
+
readonly records: T[];
|
|
3
|
+
readonly total: number;
|
|
4
|
+
readonly bufferOverflowed: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type RecordStore<K, T> = {
|
|
8
|
+
set(key: K, rec: T): void;
|
|
9
|
+
get(key: K): T | undefined;
|
|
10
|
+
delete(key: K): void;
|
|
11
|
+
values(): IterableIterator<T>;
|
|
12
|
+
// Takes the overflow flag as it pages, so each drain reports only the loss since the previous one.
|
|
13
|
+
page(matched: T[], limit: number | undefined): DrainPage<T>;
|
|
14
|
+
clear(): void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const MAX_LIMIT = 500;
|
|
18
|
+
const DEFAULT_LIMIT = 50;
|
|
19
|
+
|
|
20
|
+
export const createRecordStore = <K, T>(capacity: number): RecordStore<K, T> => {
|
|
21
|
+
const records = new Map<K, T>();
|
|
22
|
+
let overflowed = false;
|
|
23
|
+
|
|
24
|
+
const evictOldestIfFull = (): void => {
|
|
25
|
+
while (records.size >= capacity) {
|
|
26
|
+
const oldest = records.keys().next();
|
|
27
|
+
if (oldest.done) return;
|
|
28
|
+
records.delete(oldest.value);
|
|
29
|
+
overflowed = true;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
set(key, rec) {
|
|
35
|
+
evictOldestIfFull();
|
|
36
|
+
records.set(key, rec);
|
|
37
|
+
},
|
|
38
|
+
get(key) {
|
|
39
|
+
return records.get(key);
|
|
40
|
+
},
|
|
41
|
+
delete(key) {
|
|
42
|
+
records.delete(key);
|
|
43
|
+
},
|
|
44
|
+
values() {
|
|
45
|
+
return records.values();
|
|
46
|
+
},
|
|
47
|
+
page(matched, limit) {
|
|
48
|
+
const bufferOverflowed = overflowed;
|
|
49
|
+
overflowed = false;
|
|
50
|
+
return {
|
|
51
|
+
records: matched.slice(-Math.min(limit ?? DEFAULT_LIMIT, MAX_LIMIT)),
|
|
52
|
+
total: matched.length,
|
|
53
|
+
bufferOverflowed,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
clear() {
|
|
57
|
+
records.clear();
|
|
58
|
+
overflowed = false;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
};
|