@necpp-engine/wasm 0.1.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.
@@ -0,0 +1,11 @@
1
+ import type { CreateNecWorkerModelOptions, NecWorkerModel } from "./types.js";
2
+ export interface WorkerHost {
3
+ postMessage(data: unknown, transfer?: readonly ArrayBuffer[]): void;
4
+ subscribe(listener: (data: unknown) => void): () => void;
5
+ subscribeError(listener: (error: unknown) => void): () => void;
6
+ subscribeExit?(listener: (code: number) => void): () => void;
7
+ terminate(): void;
8
+ }
9
+ export declare function createNecWorkerModelFromHost(host: WorkerHost, options?: CreateNecWorkerModelOptions): Promise<NecWorkerModel>;
10
+ /** Create a stateful NEC model that runs inside a dedicated worker. */
11
+ export declare function createNecWorkerModel(options?: CreateNecWorkerModelOptions): Promise<NecWorkerModel>;
@@ -0,0 +1,325 @@
1
+ import { NecInputError, NecRuntimeError, NecStateError } from "./errors.js";
2
+ import { cloneFloat64, isNodeRuntime, isWorkerResponse, reviveEmbeddedFarFieldResult, reviveError, reviveFarFieldResult, reviveImpedanceResult, revivePortSolution, serializeCreateOptions, } from "./worker-protocol.js";
3
+ function createWebWorker() {
4
+ return new Worker(new URL("./worker-entry.js", import.meta.url), {
5
+ type: "module",
6
+ });
7
+ }
8
+ async function openWorkerHost() {
9
+ if (isNodeRuntime()) {
10
+ const { Worker: NodeWorker } = await import("node:worker_threads");
11
+ const worker = new NodeWorker(new URL("./worker-entry.js", import.meta.url));
12
+ return {
13
+ postMessage(data, transfer = []) {
14
+ worker.postMessage(data, transfer);
15
+ },
16
+ subscribe(listener) {
17
+ const handler = (value) => {
18
+ listener(value);
19
+ };
20
+ worker.on("message", handler);
21
+ return () => {
22
+ worker.off("message", handler);
23
+ };
24
+ },
25
+ subscribeError(listener) {
26
+ const handler = (error) => {
27
+ listener(error);
28
+ };
29
+ worker.on("error", handler);
30
+ return () => {
31
+ worker.off("error", handler);
32
+ };
33
+ },
34
+ subscribeExit(listener) {
35
+ const handler = (code) => {
36
+ listener(code);
37
+ };
38
+ worker.on("exit", handler);
39
+ return () => {
40
+ worker.off("exit", handler);
41
+ };
42
+ },
43
+ terminate() {
44
+ void worker.terminate();
45
+ },
46
+ };
47
+ }
48
+ const worker = createWebWorker();
49
+ return {
50
+ postMessage(data, transfer = []) {
51
+ worker.postMessage(data, transfer);
52
+ },
53
+ subscribe(listener) {
54
+ const handler = (event) => {
55
+ listener(event.data);
56
+ };
57
+ worker.addEventListener("message", handler);
58
+ return () => {
59
+ worker.removeEventListener("message", handler);
60
+ };
61
+ },
62
+ subscribeError(listener) {
63
+ const errorHandler = (event) => {
64
+ listener(event.error ?? event.message);
65
+ };
66
+ const messageErrorHandler = () => {
67
+ listener(new Error("The NEC worker could not deserialize a message"));
68
+ };
69
+ worker.addEventListener("error", errorHandler);
70
+ worker.addEventListener("messageerror", messageErrorHandler);
71
+ return () => {
72
+ worker.removeEventListener("error", errorHandler);
73
+ worker.removeEventListener("messageerror", messageErrorHandler);
74
+ };
75
+ },
76
+ terminate() {
77
+ worker.terminate();
78
+ },
79
+ };
80
+ }
81
+ function cloneComplexVector(vector) {
82
+ const real = cloneFloat64(vector.real);
83
+ const imag = cloneFloat64(vector.imag);
84
+ return {
85
+ vector: { real, imag },
86
+ transfer: [real.buffer, imag.buffer],
87
+ };
88
+ }
89
+ class WorkerNecModel {
90
+ #host;
91
+ #state = "empty";
92
+ #terminated = false;
93
+ #nextId = 1;
94
+ #tail = Promise.resolve();
95
+ #pending = new Map();
96
+ #listeners = new Set();
97
+ #unsubscribeMessage;
98
+ #unsubscribeError;
99
+ #unsubscribeExit;
100
+ constructor(host, onProgress) {
101
+ this.#host = host;
102
+ if (onProgress !== undefined) {
103
+ this.#listeners.add(onProgress);
104
+ }
105
+ this.#unsubscribeMessage = host.subscribe((data) => {
106
+ this.#onMessage(data);
107
+ });
108
+ this.#unsubscribeError = host.subscribeError((error) => {
109
+ this.#failAll(new NecRuntimeError("The NEC worker failed", { cause: error }));
110
+ });
111
+ this.#unsubscribeExit = host.subscribeExit?.((code) => {
112
+ if (!this.#terminated) {
113
+ this.#failAll(new NecRuntimeError(`The NEC worker exited unexpectedly with code ${code}`, { details: { exitCode: code } }));
114
+ }
115
+ }) ?? (() => undefined);
116
+ }
117
+ get state() {
118
+ return this.#state;
119
+ }
120
+ subscribeProgress(listener) {
121
+ this.#listeners.add(listener);
122
+ return () => {
123
+ this.#listeners.delete(listener);
124
+ };
125
+ }
126
+ async initialize(options) {
127
+ const serialized = serializeCreateOptions(options);
128
+ await this.#request(serialized.payload === undefined
129
+ ? { kind: "create" }
130
+ : { kind: "create", options: serialized.payload }, serialized.transfer);
131
+ }
132
+ addWire(wire) {
133
+ return this.#invokeVoid("addWire", [wire]);
134
+ }
135
+ completeGeometry(options) {
136
+ return this.#invokeVoid("completeGeometry", options === undefined ? [] : [options]);
137
+ }
138
+ definePorts(ports) {
139
+ return this.#invokeVoid("definePorts", [ports]);
140
+ }
141
+ addLoad(load) {
142
+ return this.#invokeVoid("addLoad", [load]);
143
+ }
144
+ clearLoads() {
145
+ return this.#invokeVoid("clearLoads", []);
146
+ }
147
+ setGround(ground) {
148
+ return this.#invokeVoid("setGround", [ground]);
149
+ }
150
+ prepare(options) {
151
+ return this.#invokeVoid("prepare", [options]);
152
+ }
153
+ async computeImpedanceMatrix() {
154
+ return reviveImpedanceResult(await this.#invoke("computeImpedanceMatrix", []));
155
+ }
156
+ async solveVoltages(voltages) {
157
+ const cloned = cloneComplexVector(voltages);
158
+ return revivePortSolution(await this.#invoke("solveVoltages", [cloned.vector], cloned.transfer));
159
+ }
160
+ async solveCurrents(currents) {
161
+ const cloned = cloneComplexVector(currents);
162
+ return revivePortSolution(await this.#invoke("solveCurrents", [cloned.vector], cloned.transfer));
163
+ }
164
+ async computeFarField(request) {
165
+ return reviveFarFieldResult(await this.#invoke("computeFarField", [request]));
166
+ }
167
+ async computeEmbeddedFarFields(request, normalization) {
168
+ const args = normalization === undefined ? [request] : [request, normalization];
169
+ return reviveEmbeddedFarFieldResult(await this.#invoke("computeEmbeddedFarFields", args));
170
+ }
171
+ async dispose() {
172
+ if (this.#terminated || this.#state === "disposed") {
173
+ this.#state = "disposed";
174
+ this.terminate();
175
+ return;
176
+ }
177
+ try {
178
+ await this.#invoke("dispose", []);
179
+ }
180
+ finally {
181
+ this.terminate();
182
+ }
183
+ }
184
+ terminate() {
185
+ if (this.#terminated) {
186
+ return;
187
+ }
188
+ this.#terminated = true;
189
+ this.#state = "disposed";
190
+ this.#unsubscribeMessage();
191
+ this.#unsubscribeError();
192
+ this.#unsubscribeExit();
193
+ const error = new NecRuntimeError("The NEC worker was terminated");
194
+ for (const pending of this.#pending.values()) {
195
+ pending.reject(error);
196
+ }
197
+ this.#pending.clear();
198
+ try {
199
+ this.#host.terminate();
200
+ }
201
+ catch {
202
+ // Termination is best-effort once outstanding work has been rejected.
203
+ }
204
+ }
205
+ #notify(listenerEvent) {
206
+ for (const listener of [...this.#listeners]) {
207
+ try {
208
+ listener(listenerEvent);
209
+ }
210
+ catch {
211
+ // Progress listeners must not break the worker client.
212
+ }
213
+ }
214
+ }
215
+ #failAll(error) {
216
+ if (this.#terminated) {
217
+ return;
218
+ }
219
+ this.#terminated = true;
220
+ this.#state = "disposed";
221
+ this.#unsubscribeMessage();
222
+ this.#unsubscribeError();
223
+ this.#unsubscribeExit();
224
+ for (const pending of this.#pending.values()) {
225
+ pending.reject(error);
226
+ }
227
+ this.#pending.clear();
228
+ try {
229
+ this.#host.terminate();
230
+ }
231
+ catch {
232
+ // The host is already failing.
233
+ }
234
+ }
235
+ #onMessage(data) {
236
+ if (!isWorkerResponse(data)) {
237
+ this.#failAll(new NecRuntimeError("The NEC worker returned a malformed message"));
238
+ return;
239
+ }
240
+ if (data.kind === "progress") {
241
+ this.#notify(data);
242
+ return;
243
+ }
244
+ if (data.kind === "crash") {
245
+ this.#failAll(new NecRuntimeError(data.error.message, data.error.details === undefined ? {} : { details: data.error.details }));
246
+ return;
247
+ }
248
+ const pending = this.#pending.get(data.id);
249
+ if (pending === undefined) {
250
+ return;
251
+ }
252
+ this.#pending.delete(data.id);
253
+ if (data.state !== undefined) {
254
+ this.#state = data.state;
255
+ }
256
+ if (data.kind === "ok") {
257
+ pending.resolve(data.result);
258
+ return;
259
+ }
260
+ pending.reject(reviveError(data.error));
261
+ }
262
+ #assertCallable(method) {
263
+ if (this.#terminated || this.#state === "disposed") {
264
+ if (method === "dispose") {
265
+ return;
266
+ }
267
+ throw new NecStateError(method, "disposed");
268
+ }
269
+ }
270
+ #request(body, transfer = []) {
271
+ this.#assertCallable(body.kind === "create" ? "create" : body.method);
272
+ const run = this.#tail.then(() => {
273
+ if (this.#terminated) {
274
+ throw new NecRuntimeError("The NEC worker was terminated");
275
+ }
276
+ return new Promise((resolve, reject) => {
277
+ const id = this.#nextId;
278
+ this.#nextId += 1;
279
+ this.#pending.set(id, { resolve, reject });
280
+ try {
281
+ this.#host.postMessage({ ...body, id }, transfer);
282
+ }
283
+ catch (cause) {
284
+ this.#pending.delete(id);
285
+ reject(new NecRuntimeError("Failed to post a NEC worker request", {
286
+ cause,
287
+ }));
288
+ }
289
+ });
290
+ });
291
+ this.#tail = run.then(() => undefined, () => undefined);
292
+ return run;
293
+ }
294
+ #invokeVoid(method, args, transfer = []) {
295
+ return this.#invoke(method, args, transfer).then(() => undefined);
296
+ }
297
+ async #invoke(method, args, transfer = []) {
298
+ if (method !== "dispose") {
299
+ this.#assertCallable(method);
300
+ }
301
+ else if (this.#terminated || this.#state === "disposed") {
302
+ return undefined;
303
+ }
304
+ return this.#request({ kind: "invoke", method, args }, transfer);
305
+ }
306
+ }
307
+ export async function createNecWorkerModelFromHost(host, options) {
308
+ const model = new WorkerNecModel(host, options?.onProgress);
309
+ try {
310
+ await model.initialize(options);
311
+ return model;
312
+ }
313
+ catch (error) {
314
+ model.terminate();
315
+ throw error;
316
+ }
317
+ }
318
+ /** Create a stateful NEC model that runs inside a dedicated worker. */
319
+ export async function createNecWorkerModel(options) {
320
+ if (options !== undefined && (typeof options !== "object" || options === null)) {
321
+ throw new NecInputError("WASM loading options must be an object");
322
+ }
323
+ const host = await openWorkerHost();
324
+ return createNecWorkerModelFromHost(host, options);
325
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,93 @@
1
+ import { createNecModel } from "./index.js";
2
+ import { NecRuntimeError } from "./errors.js";
3
+ import { isNodeRuntime, isWorkerResponse, serializeError, } from "./worker-protocol.js";
4
+ import { handleWorkerRequest } from "./worker-runtime.js";
5
+ async function connectParent() {
6
+ if (typeof document !== "undefined") {
7
+ throw new NecRuntimeError("NEC worker entry must run inside a worker");
8
+ }
9
+ if (isNodeRuntime()) {
10
+ const { parentPort } = await import("node:worker_threads");
11
+ if (parentPort === null) {
12
+ throw new NecRuntimeError("NEC worker entry must run inside a worker");
13
+ }
14
+ return {
15
+ postMessage(value, transfer = []) {
16
+ parentPort.postMessage(value, transfer);
17
+ },
18
+ onMessage(listener) {
19
+ parentPort.on("message", listener);
20
+ },
21
+ };
22
+ }
23
+ const scope = globalThis;
24
+ if (typeof scope.postMessage !== "function") {
25
+ throw new NecRuntimeError("NEC worker entry must run inside a worker");
26
+ }
27
+ return {
28
+ postMessage(value, transfer = []) {
29
+ scope.postMessage(value, transfer);
30
+ },
31
+ onMessage(listener) {
32
+ scope.addEventListener("message", (event) => {
33
+ listener(event.data);
34
+ });
35
+ },
36
+ };
37
+ }
38
+ function isWorkerRequest(value) {
39
+ if (typeof value !== "object" || value === null) {
40
+ return false;
41
+ }
42
+ const record = value;
43
+ return typeof record.id === "number"
44
+ && (record.kind === "create" || record.kind === "invoke");
45
+ }
46
+ void startWorker();
47
+ async function startWorker() {
48
+ const parent = await connectParent();
49
+ const session = { model: undefined };
50
+ let queue = Promise.resolve();
51
+ function post(response, transfer = []) {
52
+ parent.postMessage(response, transfer);
53
+ }
54
+ parent.onMessage((value) => {
55
+ queue = queue.then(async () => {
56
+ if (!isWorkerRequest(value)) {
57
+ post({
58
+ kind: "crash",
59
+ error: serializeError(new NecRuntimeError("The NEC worker received a malformed request")),
60
+ });
61
+ return;
62
+ }
63
+ try {
64
+ const { response, transfer } = await handleWorkerRequest(session, value, {
65
+ createModel: createNecModel,
66
+ emitProgress(event) {
67
+ post({
68
+ kind: "progress",
69
+ operation: event.operation,
70
+ phase: event.phase,
71
+ });
72
+ },
73
+ });
74
+ if (!isWorkerResponse(response)) {
75
+ post({
76
+ id: value.id,
77
+ kind: "error",
78
+ error: serializeError(new NecRuntimeError("The NEC worker produced an invalid response")),
79
+ });
80
+ return;
81
+ }
82
+ post(response, transfer);
83
+ }
84
+ catch (error) {
85
+ post({
86
+ id: value.id,
87
+ kind: "error",
88
+ error: serializeError(error),
89
+ });
90
+ }
91
+ }, () => undefined);
92
+ });
93
+ }
@@ -0,0 +1,62 @@
1
+ import { NecError, type NecErrorCode } from "./errors.js";
2
+ import type { CreateNecModelOptions, CreateNecWorkerModelOptions, EmbeddedFarFieldResult, FarFieldResult, ImpedanceResult, NecModelState, NecWorkerOperation, PortDefinition, PortSolution } from "./types.js";
3
+ export declare function isNodeRuntime(): boolean;
4
+ export type WorkerMethod = Exclude<NecWorkerOperation, "create">;
5
+ export interface SerializedCreateOptions {
6
+ readonly wasmUrl?: string;
7
+ readonly wasmBinary?: ArrayBuffer;
8
+ }
9
+ export type WorkerRequest = {
10
+ readonly id: number;
11
+ readonly kind: "create";
12
+ readonly options?: SerializedCreateOptions;
13
+ } | {
14
+ readonly id: number;
15
+ readonly kind: "invoke";
16
+ readonly method: WorkerMethod;
17
+ readonly args: readonly unknown[];
18
+ };
19
+ export interface SerializedNecError {
20
+ readonly name: string;
21
+ readonly code: NecErrorCode;
22
+ readonly message: string;
23
+ readonly details?: Readonly<Record<string, unknown>>;
24
+ readonly operation?: string;
25
+ readonly state?: NecModelState;
26
+ }
27
+ export type WorkerProgressMessage = {
28
+ readonly kind: "progress";
29
+ readonly operation: NecWorkerOperation;
30
+ readonly phase: "start" | "complete";
31
+ };
32
+ export type WorkerResponse = {
33
+ readonly id: number;
34
+ readonly kind: "ok";
35
+ readonly state: NecModelState;
36
+ readonly result?: unknown;
37
+ readonly transferredBufferCount: number;
38
+ } | {
39
+ readonly id: number;
40
+ readonly kind: "error";
41
+ readonly state?: NecModelState;
42
+ readonly error: SerializedNecError;
43
+ } | {
44
+ readonly kind: "crash";
45
+ readonly error: SerializedNecError;
46
+ } | WorkerProgressMessage;
47
+ export declare function isWorkerResponse(value: unknown): value is WorkerResponse;
48
+ /** Collect unique, non-empty ArrayBuffers owned by a structured-cloneable result. */
49
+ export declare function collectTransferables(value: unknown): ArrayBuffer[];
50
+ export declare function serializeError(error: unknown): SerializedNecError;
51
+ export declare function reviveError(serialized: SerializedNecError): NecError;
52
+ export declare function snapshotPorts(ports: readonly PortDefinition[]): readonly PortDefinition[];
53
+ export declare function revivePortSolution(value: unknown): PortSolution;
54
+ export declare function reviveImpedanceResult(value: unknown): ImpedanceResult;
55
+ export declare function reviveFarFieldResult(value: unknown): FarFieldResult;
56
+ export declare function reviveEmbeddedFarFieldResult(value: unknown): EmbeddedFarFieldResult;
57
+ export declare function cloneFloat64(source: Float64Array): Float64Array<ArrayBuffer>;
58
+ export declare function serializeCreateOptions(options: CreateNecWorkerModelOptions | undefined): {
59
+ readonly payload?: SerializedCreateOptions;
60
+ readonly transfer: ArrayBuffer[];
61
+ };
62
+ export declare function toCreateNecModelOptions(options: SerializedCreateOptions | undefined): CreateNecModelOptions | undefined;