@get-bb/plugin-sdk 0.4.6 → 0.4.9

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.
@@ -0,0 +1,165 @@
1
+ // src/testing/host.ts
2
+ var RESULT_MAX_BYTES = 8 * 1024 * 1024;
3
+ async function validate(schema, value) {
4
+ const result = await schema["~standard"].validate(value);
5
+ if (result.issues !== void 0) {
6
+ throw new Error(result.issues.map((issue) => issue.message).join("; "));
7
+ }
8
+ return result.value;
9
+ }
10
+ function normalizeJson(value, label) {
11
+ let serialized;
12
+ try {
13
+ serialized = JSON.stringify(value);
14
+ } catch {
15
+ serialized = void 0;
16
+ }
17
+ if (serialized === void 0) {
18
+ throw new Error(`${label} is not JSON-serializable`);
19
+ }
20
+ if (Buffer.byteLength(serialized) > RESULT_MAX_BYTES) {
21
+ throw new Error(`${label} exceeds ${RESULT_MAX_BYTES} bytes`);
22
+ }
23
+ return JSON.parse(serialized);
24
+ }
25
+ function experimental_createHostEntryHarness(entry, harnessOptions = {}) {
26
+ const lifecycleController = new AbortController();
27
+ const activeCalls = /* @__PURE__ */ new Set();
28
+ const capturedSignals = [];
29
+ const watchSubscriptions = /* @__PURE__ */ new Set();
30
+ const retainedWorkerLeases = /* @__PURE__ */ new Set();
31
+ const paths = harnessOptions.experimental_paths ?? {
32
+ dataDir: "/test/plugin-data",
33
+ tempDir: "/test/plugin-temp"
34
+ };
35
+ let disposePromise = null;
36
+ return {
37
+ get experimental_lifecycleSignal() {
38
+ return lifecycleController.signal;
39
+ },
40
+ async experimental_call(methodName, input, options = {}) {
41
+ if (disposePromise !== null) {
42
+ throw new Error("host entry harness is disposed");
43
+ }
44
+ const method = entry.contract[methodName];
45
+ const handler = entry.handlers[methodName];
46
+ if (method === void 0 || handler === void 0) {
47
+ throw new Error(`unknown host method "${String(methodName)}"`);
48
+ }
49
+ const controller = new AbortController();
50
+ activeCalls.add(controller);
51
+ const abort = () => controller.abort();
52
+ lifecycleController.signal.addEventListener("abort", abort, {
53
+ once: true
54
+ });
55
+ options.signal?.addEventListener("abort", abort, { once: true });
56
+ if (lifecycleController.signal.aborted || options.signal?.aborted === true) {
57
+ controller.abort();
58
+ }
59
+ let contextOpen = true;
60
+ try {
61
+ const serverInput = await validate(method.input, input);
62
+ const workerInput = await validate(
63
+ method.input,
64
+ normalizeJson(serverInput, `host input for ${methodName}`)
65
+ );
66
+ const rawOutput = await handler(workerInput, {
67
+ signal: controller.signal,
68
+ lifecycle: { signal: lifecycleController.signal },
69
+ experimental_paths: paths,
70
+ async experimental_emitSignal(signalName, payload) {
71
+ const descriptor = entry.experimental_signals?.[signalName];
72
+ if (descriptor === void 0) {
73
+ throw new Error(`unknown host signal "${String(signalName)}"`);
74
+ }
75
+ const workerPayload = await validate(descriptor.payload, payload);
76
+ const serverPayload = await validate(
77
+ descriptor.payload,
78
+ normalizeJson(workerPayload, `host signal ${String(signalName)}`)
79
+ );
80
+ capturedSignals.push({
81
+ signal: String(signalName),
82
+ payload: serverPayload
83
+ });
84
+ },
85
+ async experimental_watch(watchOptions, listener) {
86
+ if (lifecycleController.signal.aborted) {
87
+ throw new Error("host entry harness is disposed");
88
+ }
89
+ const resolvedWatchOptions = {
90
+ rootPath: watchOptions.rootPath,
91
+ ignoredPaths: watchOptions.ignoredPaths ?? [],
92
+ debounceMs: watchOptions.debounceMs ?? 75,
93
+ maxWaitMs: watchOptions.maxWaitMs ?? 500
94
+ };
95
+ const underlying = await (harnessOptions.experimental_watch?.(
96
+ resolvedWatchOptions,
97
+ listener
98
+ ) ?? { dispose: async () => void 0 });
99
+ let disposed = false;
100
+ const subscription = {
101
+ async dispose() {
102
+ if (disposed) return;
103
+ disposed = true;
104
+ watchSubscriptions.delete(subscription);
105
+ await underlying.dispose();
106
+ }
107
+ };
108
+ watchSubscriptions.add(subscription);
109
+ return subscription;
110
+ },
111
+ experimental_retainWorker() {
112
+ if (!contextOpen || controller.signal.aborted || lifecycleController.signal.aborted) {
113
+ throw new Error("host call context is no longer active");
114
+ }
115
+ let disposed = false;
116
+ const lease = {
117
+ async dispose() {
118
+ if (disposed) return;
119
+ disposed = true;
120
+ retainedWorkerLeases.delete(lease);
121
+ }
122
+ };
123
+ retainedWorkerLeases.add(lease);
124
+ return lease;
125
+ }
126
+ });
127
+ const workerOutput = await validate(method.output, rawOutput);
128
+ return await validate(
129
+ method.output,
130
+ normalizeJson(workerOutput, `host output for ${methodName}`)
131
+ );
132
+ } finally {
133
+ contextOpen = false;
134
+ activeCalls.delete(controller);
135
+ lifecycleController.signal.removeEventListener("abort", abort);
136
+ options.signal?.removeEventListener("abort", abort);
137
+ }
138
+ },
139
+ experimental_getSignals() {
140
+ return capturedSignals;
141
+ },
142
+ experimental_getRetainedWorkerLeaseCount() {
143
+ return retainedWorkerLeases.size;
144
+ },
145
+ experimental_dispose() {
146
+ disposePromise ??= (async () => {
147
+ lifecycleController.abort();
148
+ for (const call of activeCalls) call.abort();
149
+ activeCalls.clear();
150
+ await Promise.all(
151
+ [...watchSubscriptions].map((subscription) => subscription.dispose())
152
+ );
153
+ try {
154
+ await entry.dispose?.();
155
+ } finally {
156
+ retainedWorkerLeases.clear();
157
+ }
158
+ })();
159
+ return disposePromise;
160
+ }
161
+ };
162
+ }
163
+ export {
164
+ experimental_createHostEntryHarness
165
+ };