@tsplc/server 0.0.0-stage → 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.
- package/README.md +6 -2
- package/dist/hmi.js +132 -0
- package/dist/hmi.js.map +11 -0
- package/dist/index.js +320 -0
- package/dist/index.js.map +11 -0
- package/dist/opcua.js +484 -0
- package/dist/opcua.js.map +12 -0
- package/dist/runtime.js +293 -0
- package/dist/runtime.js.map +12 -0
- package/dist/serve.js +711 -0
- package/dist/serve.js.map +14 -0
- package/package.json +48 -4
- package/src/contract.ts +366 -0
- package/src/hmi-page.ts +111 -0
- package/src/hmi.ts +127 -0
- package/src/index.ts +106 -0
- package/src/opcua.ts +211 -0
- package/src/serve.ts +170 -0
package/dist/opcua.js
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
// packages/server/src/opcua.ts
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
+
import { AttributeIds, DataType, discoverVariables } from "@tsplc/opcua";
|
|
5
|
+
|
|
6
|
+
// packages/server/src/index.ts
|
|
7
|
+
import { TRPCError } from "@trpc/server";
|
|
8
|
+
|
|
9
|
+
// packages/server/src/contract.ts
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
var integerTypes = {
|
|
13
|
+
SINT: [-128, 127],
|
|
14
|
+
USINT: [0, 255],
|
|
15
|
+
BYTE: [0, 255],
|
|
16
|
+
INT: [-32768, 32767],
|
|
17
|
+
UINT: [0, 65535],
|
|
18
|
+
WORD: [0, 65535],
|
|
19
|
+
DINT: [-2147483648, 2147483647],
|
|
20
|
+
UDINT: [0, 4294967295],
|
|
21
|
+
DWORD: [0, 4294967295]
|
|
22
|
+
};
|
|
23
|
+
var integer = (type) => (bounds = {}) => z.number().int().min(bounds.min ?? integerTypes[type][0]).max(bounds.max ?? integerTypes[type][1]).meta({ iec: type });
|
|
24
|
+
var iec = {
|
|
25
|
+
SINT: integer("SINT"),
|
|
26
|
+
USINT: integer("USINT"),
|
|
27
|
+
BYTE: integer("BYTE"),
|
|
28
|
+
INT: integer("INT"),
|
|
29
|
+
UINT: integer("UINT"),
|
|
30
|
+
WORD: integer("WORD"),
|
|
31
|
+
DINT: integer("DINT"),
|
|
32
|
+
UDINT: integer("UDINT"),
|
|
33
|
+
DWORD: integer("DWORD"),
|
|
34
|
+
STRING: (capacity) => z.string().max(capacity),
|
|
35
|
+
FixedArray: (element, length) => z.array(element).length(length)
|
|
36
|
+
};
|
|
37
|
+
var identifier = /^(?!__proto__$)[A-Za-z_][A-Za-z0-9_]*$/;
|
|
38
|
+
var isIntegerType = (value) => Object.hasOwn(integerTypes, value);
|
|
39
|
+
var wireScalar = (field, label) => {
|
|
40
|
+
switch (field.kind) {
|
|
41
|
+
case "boolean":
|
|
42
|
+
return { kind: "boolean" };
|
|
43
|
+
case "string":
|
|
44
|
+
if (!Number.isInteger(field.capacity) || field.capacity < 1 || field.capacity > 2048)
|
|
45
|
+
throw new Error(`${label}: string capacity must be 1..2048 bytes.`);
|
|
46
|
+
return { kind: "string", capacity: field.capacity };
|
|
47
|
+
case "integer": {
|
|
48
|
+
const [minimum, maximum] = integerTypes[field.iec];
|
|
49
|
+
if (!Number.isInteger(field.minimum) || !Number.isInteger(field.maximum))
|
|
50
|
+
throw new Error(`${label}: integer bounds must be integers.`);
|
|
51
|
+
if (field.minimum > field.maximum)
|
|
52
|
+
throw new Error(`${label}: integer bounds are contradictory.`);
|
|
53
|
+
if (field.minimum < minimum || field.maximum > maximum)
|
|
54
|
+
throw new Error(`${label}: integer bounds do not fit ${field.iec}.`);
|
|
55
|
+
return { kind: "integer", iec: field.iec, minimum: field.minimum, maximum: field.maximum };
|
|
56
|
+
}
|
|
57
|
+
case "enum": {
|
|
58
|
+
const values = Object.entries(field.members).sort((left, right) => {
|
|
59
|
+
if (left[1] === 0)
|
|
60
|
+
return -1;
|
|
61
|
+
if (right[1] === 0)
|
|
62
|
+
return 1;
|
|
63
|
+
return left[1] - right[1];
|
|
64
|
+
});
|
|
65
|
+
if (!values.length || values.some(([name, value]) => !identifier.test(name) || !Number.isInteger(value) || value < -2147483648 || value > 2147483647))
|
|
66
|
+
throw new Error(`${label}: PLC enums require named DINT values.`);
|
|
67
|
+
if (new Set(values.map(([, value]) => value)).size !== values.length)
|
|
68
|
+
throw new Error(`${label}: PLC enum values must be distinct.`);
|
|
69
|
+
return { kind: "enum", members: Object.fromEntries(values) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var wireRecord = (fields, label, output) => {
|
|
74
|
+
const checked = {};
|
|
75
|
+
for (const key of Object.keys(fields).sort()) {
|
|
76
|
+
const field = fields[key];
|
|
77
|
+
if (!identifier.test(key))
|
|
78
|
+
throw new Error(`${label}.${key}: use an IEC field identifier.`);
|
|
79
|
+
if (field.kind !== "array") {
|
|
80
|
+
checked[key] = wireScalar(field, `${label}.${key}`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (output)
|
|
84
|
+
throw new Error(`${label}.${key}: the native HTTP decoder currently supports scalar response fields only.`);
|
|
85
|
+
if (!Number.isInteger(field.length) || field.length < 1 || field.length > 256)
|
|
86
|
+
throw new Error(`${label}.${key}: arrays require one fixed length from 1 to 256.`);
|
|
87
|
+
checked[key] = {
|
|
88
|
+
kind: "array",
|
|
89
|
+
length: field.length,
|
|
90
|
+
element: wireScalar(field.element, `${label}.${key}`)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (Object.keys(checked).length > 31 || output && !Object.keys(checked).length)
|
|
94
|
+
throw new Error(`${label}: response records require 1..31 fields; requests allow 0..31.`);
|
|
95
|
+
return { fields: checked };
|
|
96
|
+
};
|
|
97
|
+
var serviceContract = (path, kind, input, output) => {
|
|
98
|
+
if (path.split(".").some((part) => !identifier.test(part)))
|
|
99
|
+
throw new Error(`${path}: PLC procedure paths require identifier segments.`);
|
|
100
|
+
const version = createHash("sha256").update(JSON.stringify({ abi: 2, path, kind, input, output })).digest("hex").slice(0, 24);
|
|
101
|
+
return { path, kind, input, output, version };
|
|
102
|
+
};
|
|
103
|
+
var schemaValue = (value) => typeof value === "object" && value !== null && ("_zod" in value) && ("safeParse" in value);
|
|
104
|
+
var schema = (value, label) => {
|
|
105
|
+
if (!schemaValue(value))
|
|
106
|
+
throw new Error(`${label}: provide a bounded Zod input/output schema; TypeScript number/string inference has no IEC memory layout.`);
|
|
107
|
+
return value;
|
|
108
|
+
};
|
|
109
|
+
var jsonSchema = z.looseObject({
|
|
110
|
+
type: z.string().optional(),
|
|
111
|
+
maxLength: z.number().optional(),
|
|
112
|
+
minItems: z.number().optional(),
|
|
113
|
+
maxItems: z.number().optional(),
|
|
114
|
+
minimum: z.number().optional(),
|
|
115
|
+
maximum: z.number().optional(),
|
|
116
|
+
iec: z.string().optional()
|
|
117
|
+
});
|
|
118
|
+
var metadata = (value) => jsonSchema.parse(value.meta() ?? {});
|
|
119
|
+
var refSchema = z.looseObject({
|
|
120
|
+
$ref: z.string().optional(),
|
|
121
|
+
$defs: z.record(z.string(), z.unknown()).optional()
|
|
122
|
+
});
|
|
123
|
+
var jsonFor = (value, options) => {
|
|
124
|
+
const json = refSchema.parse(z.toJSONSchema(value, options));
|
|
125
|
+
const definition = json.$ref?.startsWith("#/$defs/") ? json.$defs?.[json.$ref.slice("#/$defs/".length)] : json;
|
|
126
|
+
return jsonSchema.parse(definition);
|
|
127
|
+
};
|
|
128
|
+
var scalar = (value, label) => {
|
|
129
|
+
const definition = value._zod.def;
|
|
130
|
+
if (["optional", "nullable", "union", "default", "prefault", "pipe", "catch"].includes(definition.type))
|
|
131
|
+
throw new Error(`${label}: optional, nullable, transformed and defaulted wire fields are unsupported.`);
|
|
132
|
+
if (definition.type === "enum") {
|
|
133
|
+
const entries = z.record(z.string(), z.union([z.number().int(), z.string()])).parse(Reflect.get(definition, "entries"));
|
|
134
|
+
const members = {};
|
|
135
|
+
for (const [name, value] of Object.entries(entries)) {
|
|
136
|
+
if (typeof value === "number")
|
|
137
|
+
members[name] = value;
|
|
138
|
+
else if (identifier.test(name) && entries[value] !== name)
|
|
139
|
+
throw new Error(`${label}: PLC enums require only numeric members.`);
|
|
140
|
+
}
|
|
141
|
+
return { kind: "enum", members };
|
|
142
|
+
}
|
|
143
|
+
const json = jsonFor(value, { io: "input", unrepresentable: "throw" });
|
|
144
|
+
if (json.type === "boolean")
|
|
145
|
+
return { kind: "boolean" };
|
|
146
|
+
if (json.type === "string" && json.maxLength !== undefined)
|
|
147
|
+
return { kind: "string", capacity: json.maxLength };
|
|
148
|
+
const type = metadata(value).iec;
|
|
149
|
+
if (json.type === "integer" && type !== undefined && isIntegerType(type) && json.minimum !== undefined && json.maximum !== undefined)
|
|
150
|
+
return { kind: "integer", iec: type, minimum: json.minimum, maximum: json.maximum };
|
|
151
|
+
throw new Error(`${label}: use z.boolean(), z.enum() of a numeric enum, iec.STRING(), or an integer such as iec.INT(). Optional, nullable, union, float and dynamic layouts are unsupported.`);
|
|
152
|
+
};
|
|
153
|
+
var record = (declared, label, output) => {
|
|
154
|
+
const piped = declared._zod.def;
|
|
155
|
+
const value = !output && piped.type === "pipe" ? schema(Reflect.get(piped, "in"), label) : declared;
|
|
156
|
+
const def = value._zod.def;
|
|
157
|
+
if (def.type !== "object")
|
|
158
|
+
throw new Error(`${label}: HTTP services require a closed object schema.`);
|
|
159
|
+
const shape = z.record(z.string(), z.custom(schemaValue)).parse(Reflect.get(def, "shape"));
|
|
160
|
+
const fields = {};
|
|
161
|
+
for (const [key, item] of Object.entries(shape)) {
|
|
162
|
+
if (item._zod.def.type !== "array") {
|
|
163
|
+
fields[key] = scalar(item, `${label}.${key}`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const bounds = jsonFor(item, { io: "input" });
|
|
167
|
+
if (bounds.maxItems === undefined || bounds.maxItems !== bounds.minItems)
|
|
168
|
+
throw new Error(`${label}.${key}: arrays require one fixed length from 1 to 256.`);
|
|
169
|
+
fields[key] = {
|
|
170
|
+
kind: "array",
|
|
171
|
+
length: bounds.maxItems,
|
|
172
|
+
element: scalar(schema(Reflect.get(item._zod.def, "element"), `${label}.${key}`), `${label}.${key}`)
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return wireRecord(fields, label, output);
|
|
176
|
+
};
|
|
177
|
+
var serviceContracts = (router) => {
|
|
178
|
+
const contracts = [];
|
|
179
|
+
const procedures = z.record(z.string(), z.unknown()).parse(router._def.procedures);
|
|
180
|
+
for (const [path, procedure] of Object.entries(procedures)) {
|
|
181
|
+
if (typeof procedure !== "function" || !("_def" in procedure))
|
|
182
|
+
throw new Error("Unsupported tRPC router metadata.");
|
|
183
|
+
const reflected = z.custom((value) => typeof value === "function" && ("_def" in value)).parse(procedure);
|
|
184
|
+
const def = reflected._def;
|
|
185
|
+
if (typeof def.meta !== "object" || def.meta === null || !("plc" in def.meta) || def.meta.plc !== true)
|
|
186
|
+
continue;
|
|
187
|
+
if (def.type !== "query" && def.type !== "mutation")
|
|
188
|
+
throw new Error(`${path}: subscriptions use a different transport; expose a query or mutation to the PLC.`);
|
|
189
|
+
if (def.inputs.length > 1)
|
|
190
|
+
throw new Error(`${path}: merge the input into one bounded object schema.`);
|
|
191
|
+
const input = record(def.inputs.length ? schema(def.inputs[0], `${path}.input`) : z.strictObject({}), `${path}.Input`, false);
|
|
192
|
+
const output = record(schema(Reflect.get(def, "output"), `${path}.output`), `${path}.Output`, true);
|
|
193
|
+
contracts.push(serviceContract(path, def.type, input, output));
|
|
194
|
+
}
|
|
195
|
+
return contracts.sort((a, b) => a.path.localeCompare(b.path));
|
|
196
|
+
};
|
|
197
|
+
var servicePath = (contract) => `/tsplc/services/${contract.version}/${contract.path}`;
|
|
198
|
+
var validateWireRecord = (record, value) => {
|
|
199
|
+
const scalarValue = (field, value) => {
|
|
200
|
+
switch (field.kind) {
|
|
201
|
+
case "boolean":
|
|
202
|
+
return typeof value === "boolean";
|
|
203
|
+
case "integer":
|
|
204
|
+
return typeof value === "number" && Number.isInteger(value) && value >= field.minimum && value <= field.maximum;
|
|
205
|
+
case "string":
|
|
206
|
+
return typeof value === "string" && !value.includes("\x00") && new TextEncoder().encode(value).length <= field.capacity;
|
|
207
|
+
case "enum":
|
|
208
|
+
return typeof value === "number" && Object.values(field.members).includes(value);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
212
|
+
throw new Error("Expected a closed wire record.");
|
|
213
|
+
const entries = Object.entries(value);
|
|
214
|
+
if (entries.length !== Object.keys(record.fields).length)
|
|
215
|
+
throw new Error("Wire record fields differ from the contract.");
|
|
216
|
+
for (const [name, field] of Object.entries(record.fields)) {
|
|
217
|
+
const item = Reflect.get(value, name);
|
|
218
|
+
const valid = field.kind === "array" ? Array.isArray(item) && item.length === field.length && item.every((value) => scalarValue(field.element, value)) : scalarValue(field, item);
|
|
219
|
+
if (!valid)
|
|
220
|
+
throw new Error(`Wire field ${name} violates its PLC representation.`);
|
|
221
|
+
}
|
|
222
|
+
if (new TextEncoder().encode(JSON.stringify(value)).length > 2048)
|
|
223
|
+
throw new Error("Wire record exceeds the 2048-byte native JSON limit.");
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// packages/server/src/index.ts
|
|
227
|
+
var createServiceHandler = (options) => {
|
|
228
|
+
const contracts = serviceContracts(options.router);
|
|
229
|
+
const paths = new Map(contracts.map((contract) => [servicePath(contract), contract]));
|
|
230
|
+
const response = (value, status = 200) => {
|
|
231
|
+
const body = JSON.stringify(value);
|
|
232
|
+
return new Response(body, {
|
|
233
|
+
status,
|
|
234
|
+
headers: {
|
|
235
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
236
|
+
"Content-Length": String(new TextEncoder().encode(body).length),
|
|
237
|
+
"Cache-Control": "no-store"
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
return async (request) => {
|
|
242
|
+
const path = new URL(request.url).pathname;
|
|
243
|
+
const contract = paths.get(path);
|
|
244
|
+
if (!contract)
|
|
245
|
+
return response({ error: "Unknown procedure or incompatible contract" }, 409);
|
|
246
|
+
if (request.method !== "POST")
|
|
247
|
+
return response({ error: "Use POST" }, 405);
|
|
248
|
+
const length = request.headers.get("content-length");
|
|
249
|
+
if (length !== null && (!/^\d+$/.test(length) || Number(length) > 2048))
|
|
250
|
+
return response({ error: "Request exceeds the PLC wire limit" }, 413);
|
|
251
|
+
const reader = request.body?.getReader();
|
|
252
|
+
const chunks = [];
|
|
253
|
+
let size = 0;
|
|
254
|
+
if (reader)
|
|
255
|
+
try {
|
|
256
|
+
for (;; ) {
|
|
257
|
+
const chunk = await reader.read();
|
|
258
|
+
if (chunk.done)
|
|
259
|
+
break;
|
|
260
|
+
size += chunk.value.length;
|
|
261
|
+
if (size > 2048) {
|
|
262
|
+
await reader.cancel();
|
|
263
|
+
return response({ error: "Request exceeds the PLC wire limit" }, 413);
|
|
264
|
+
}
|
|
265
|
+
chunks.push(chunk.value);
|
|
266
|
+
}
|
|
267
|
+
} finally {
|
|
268
|
+
reader.releaseLock();
|
|
269
|
+
}
|
|
270
|
+
const bytes = new Uint8Array(size);
|
|
271
|
+
let offset = 0;
|
|
272
|
+
for (const chunk of chunks) {
|
|
273
|
+
bytes.set(chunk, offset);
|
|
274
|
+
offset += chunk.length;
|
|
275
|
+
}
|
|
276
|
+
let input;
|
|
277
|
+
try {
|
|
278
|
+
input = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
279
|
+
validateWireRecord(contract.input, input);
|
|
280
|
+
} catch {
|
|
281
|
+
return response({ error: "Invalid procedure input" }, 400);
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const context = await options.createContext(request);
|
|
285
|
+
const caller = options.router.createCaller(context, { signal: request.signal });
|
|
286
|
+
const invoke = Reflect.get(caller, contract.path);
|
|
287
|
+
if (typeof invoke !== "function")
|
|
288
|
+
throw new Error("Invalid tRPC caller.");
|
|
289
|
+
const output = await invoke(input);
|
|
290
|
+
validateWireRecord(contract.output, output);
|
|
291
|
+
return response(output);
|
|
292
|
+
} catch (cause) {
|
|
293
|
+
if (cause instanceof TRPCError) {
|
|
294
|
+
const statuses = {
|
|
295
|
+
UNAUTHORIZED: 401,
|
|
296
|
+
FORBIDDEN: 403,
|
|
297
|
+
BAD_REQUEST: 400,
|
|
298
|
+
NOT_FOUND: 404,
|
|
299
|
+
CONFLICT: 409,
|
|
300
|
+
TOO_MANY_REQUESTS: 429
|
|
301
|
+
};
|
|
302
|
+
const status = statuses[cause.code];
|
|
303
|
+
if (status === undefined)
|
|
304
|
+
options.onError?.(cause);
|
|
305
|
+
return response({ error: cause.code }, status ?? 500);
|
|
306
|
+
}
|
|
307
|
+
options.onError?.(cause);
|
|
308
|
+
return response({ error: "Procedure failed" }, 500);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// packages/server/src/opcua.ts
|
|
314
|
+
var runOpcUaServices = async (options) => {
|
|
315
|
+
if (!options.channels.length)
|
|
316
|
+
return;
|
|
317
|
+
const dispatch = createServiceHandler(options);
|
|
318
|
+
const pending = new Map;
|
|
319
|
+
const invocations = new Set;
|
|
320
|
+
const writers = new Map;
|
|
321
|
+
const handled = new Map;
|
|
322
|
+
const epochs = new Map;
|
|
323
|
+
let activeSession;
|
|
324
|
+
let connectionError;
|
|
325
|
+
let nodes = new Map;
|
|
326
|
+
const write = async (session, path, value) => {
|
|
327
|
+
const nodeId = nodes.get(path);
|
|
328
|
+
if (!nodeId)
|
|
329
|
+
throw new Error(`Missing OPC UA service channel: ${path}`);
|
|
330
|
+
const status = await session.write({
|
|
331
|
+
nodeId,
|
|
332
|
+
attributeId: AttributeIds.Value,
|
|
333
|
+
value: { value: { dataType: DataType.String, value } }
|
|
334
|
+
});
|
|
335
|
+
if (!status.isGood())
|
|
336
|
+
throw new Error(`OPC UA service write failed: ${status.toString()}`);
|
|
337
|
+
};
|
|
338
|
+
const read = async (session, paths) => {
|
|
339
|
+
const requests = paths.map((path) => {
|
|
340
|
+
const nodeId = nodes.get(path);
|
|
341
|
+
if (!nodeId)
|
|
342
|
+
throw new Error(`Missing OPC UA service channel: ${path}`);
|
|
343
|
+
return { nodeId, attributeId: AttributeIds.Value };
|
|
344
|
+
});
|
|
345
|
+
const values = await session.read(requests, 0);
|
|
346
|
+
if (values.length !== requests.length || values.some((value) => !value.statusCode.isGood()))
|
|
347
|
+
throw new Error("OPC UA service read failed");
|
|
348
|
+
return values.map((value) => value.value.value);
|
|
349
|
+
};
|
|
350
|
+
const reply = async (session, channel, identity, body, signal) => {
|
|
351
|
+
const previous = writers.get(channel.reply) ?? Promise.resolve();
|
|
352
|
+
const writing = previous.catch(() => {
|
|
353
|
+
return;
|
|
354
|
+
}).then(async () => {
|
|
355
|
+
if (signal.aborted || activeSession !== session)
|
|
356
|
+
return;
|
|
357
|
+
await write(session, channel.reply, body);
|
|
358
|
+
await write(session, channel.replyId, identity);
|
|
359
|
+
});
|
|
360
|
+
writers.set(channel.reply, writing);
|
|
361
|
+
try {
|
|
362
|
+
await writing;
|
|
363
|
+
} finally {
|
|
364
|
+
if (writers.get(channel.reply) === writing)
|
|
365
|
+
writers.delete(channel.reply);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
try {
|
|
369
|
+
while (!options.signal.aborted) {
|
|
370
|
+
try {
|
|
371
|
+
const session = options.getSession();
|
|
372
|
+
if (session !== activeSession) {
|
|
373
|
+
for (const work of pending.values())
|
|
374
|
+
work.controller.abort();
|
|
375
|
+
nodes = await discoverVariables(session, options.channels.flatMap((channel) => [
|
|
376
|
+
channel.request,
|
|
377
|
+
channel.requestId,
|
|
378
|
+
channel.reply,
|
|
379
|
+
channel.replyId,
|
|
380
|
+
channel.epoch
|
|
381
|
+
]));
|
|
382
|
+
epochs.clear();
|
|
383
|
+
for (const channel of options.channels) {
|
|
384
|
+
await write(session, channel.replyId, "");
|
|
385
|
+
const epoch = randomBytes(16).toString("hex");
|
|
386
|
+
await write(session, channel.epoch, epoch);
|
|
387
|
+
epochs.set(channel.request, epoch);
|
|
388
|
+
}
|
|
389
|
+
activeSession = session;
|
|
390
|
+
handled.clear();
|
|
391
|
+
options.onReady?.(session);
|
|
392
|
+
}
|
|
393
|
+
const before = await read(session, options.channels.map((channel) => channel.requestId));
|
|
394
|
+
const batch = await read(session, options.channels.flatMap((channel) => [channel.request, channel.epoch]));
|
|
395
|
+
const after = await read(session, options.channels.map((channel) => channel.requestId));
|
|
396
|
+
for (const [index, channel] of options.channels.entries()) {
|
|
397
|
+
const identity = after[index];
|
|
398
|
+
if (typeof identity !== "string")
|
|
399
|
+
throw new Error("Invalid OPC UA service identity type");
|
|
400
|
+
const current = pending.get(channel.request);
|
|
401
|
+
if (current && current.identity !== identity)
|
|
402
|
+
current.controller.abort();
|
|
403
|
+
const request = batch[index * 2], epochValue = batch[index * 2 + 1];
|
|
404
|
+
if (epochValue === "") {
|
|
405
|
+
const epoch = randomBytes(16).toString("hex");
|
|
406
|
+
await write(session, channel.epoch, epoch);
|
|
407
|
+
epochs.set(channel.request, epoch);
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (before[index] !== identity || !identity)
|
|
411
|
+
continue;
|
|
412
|
+
const epoch = epochs.get(channel.request);
|
|
413
|
+
if (epochValue !== epoch)
|
|
414
|
+
throw new Error("OPC UA service gateway ownership changed");
|
|
415
|
+
if (typeof request !== "string" || !identity.startsWith(epoch + ":") || !request.startsWith(identity + "|"))
|
|
416
|
+
continue;
|
|
417
|
+
if (handled.get(channel.request) === identity)
|
|
418
|
+
continue;
|
|
419
|
+
handled.set(channel.request, identity);
|
|
420
|
+
const controller = new AbortController;
|
|
421
|
+
const signal = AbortSignal.any([options.signal, controller.signal]);
|
|
422
|
+
const operation = (async () => {
|
|
423
|
+
if (invocations.size >= options.channels.length * 2) {
|
|
424
|
+
await reply(session, channel, identity, `${identity}|1|{}`, signal);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const invocation = dispatch(new Request("http://tsplc.internal" + channel.path, {
|
|
428
|
+
method: "POST",
|
|
429
|
+
body: request.slice(identity.length + 1),
|
|
430
|
+
signal
|
|
431
|
+
}));
|
|
432
|
+
invocations.add(invocation);
|
|
433
|
+
invocation.then(() => invocations.delete(invocation), () => invocations.delete(invocation));
|
|
434
|
+
const timer = new AbortController;
|
|
435
|
+
const response = await Promise.race([
|
|
436
|
+
invocation,
|
|
437
|
+
delay(5000, undefined, { signal: AbortSignal.any([signal, timer.signal]) }).then(() => {
|
|
438
|
+
throw new Error("OPC UA service handler timed out");
|
|
439
|
+
})
|
|
440
|
+
]).finally(() => timer.abort());
|
|
441
|
+
const body = `${identity}|${response.ok ? 0 : 1}|${response.ok ? await response.text() : "{}"}`;
|
|
442
|
+
if (Buffer.byteLength(body, "utf8") > 2048)
|
|
443
|
+
throw new Error("OPC UA service reply exceeds 2048 bytes");
|
|
444
|
+
await reply(session, channel, identity, body, signal);
|
|
445
|
+
})().catch((error) => {
|
|
446
|
+
if (!signal.aborted)
|
|
447
|
+
options.onError(error);
|
|
448
|
+
}).finally(() => {
|
|
449
|
+
if (pending.get(channel.request)?.controller === controller)
|
|
450
|
+
pending.delete(channel.request);
|
|
451
|
+
});
|
|
452
|
+
pending.set(channel.request, { identity, controller, done: operation });
|
|
453
|
+
}
|
|
454
|
+
connectionError = undefined;
|
|
455
|
+
} catch (error) {
|
|
456
|
+
activeSession = undefined;
|
|
457
|
+
for (const work of pending.values())
|
|
458
|
+
work.controller.abort();
|
|
459
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
460
|
+
if (connectionError !== message) {
|
|
461
|
+
connectionError = message;
|
|
462
|
+
options.onError(error);
|
|
463
|
+
}
|
|
464
|
+
await delay(1000, undefined, { signal: options.signal });
|
|
465
|
+
}
|
|
466
|
+
await delay(20, undefined, { signal: options.signal });
|
|
467
|
+
}
|
|
468
|
+
} catch (error) {
|
|
469
|
+
if (!options.signal.aborted)
|
|
470
|
+
throw error;
|
|
471
|
+
} finally {
|
|
472
|
+
activeSession = undefined;
|
|
473
|
+
for (const work of pending.values())
|
|
474
|
+
work.controller.abort();
|
|
475
|
+
await Promise.all([...pending.values()].map((work) => work.done));
|
|
476
|
+
await Promise.allSettled(writers.values());
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
export {
|
|
480
|
+
runOpcUaServices
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
//# debugId=5CEF2E462C80B5EC64756E2164756E21
|
|
484
|
+
//# sourceMappingURL=opcua.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/opcua.ts", "../src/index.ts", "../src/contract.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { randomBytes } from \"node:crypto\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport type { AnyRouter, inferRouterContext } from \"@trpc/server\";\nimport { AttributeIds, DataType, type ClientSession, discoverVariables } from \"@tsplc/opcua\";\nimport { createServiceHandler } from \"./index\";\n\nexport interface ServiceChannel {\n path: string;\n request: string;\n requestId: string;\n reply: string;\n replyId: string;\n epoch: string;\n}\n\n/** One authenticated gateway owns these channels. Handler effects are never retried. */\nexport const runOpcUaServices = async <Router extends AnyRouter>(options: {\n router: Router;\n channels: readonly ServiceChannel[];\n getSession: () => ClientSession;\n createContext: () => inferRouterContext<Router> | Promise<inferRouterContext<Router>>;\n signal: AbortSignal;\n onError: (error: unknown) => void;\n onReady?: (session: ClientSession) => void;\n}) => {\n if (!options.channels.length) return;\n const dispatch = createServiceHandler(options);\n const pending = new Map<\n string,\n { identity: string; controller: AbortController; done: Promise<void> }\n >();\n const invocations = new Set<Promise<Response>>();\n const writers = new Map<string, Promise<void>>();\n const handled = new Map<string, string>();\n const epochs = new Map<string, string>();\n let activeSession: ClientSession | undefined;\n let connectionError: string | undefined;\n let nodes = new Map<string, string>();\n const write = async (session: ClientSession, path: string, value: string) => {\n const nodeId = nodes.get(path);\n if (!nodeId) throw new Error(`Missing OPC UA service channel: ${path}`);\n const status = await session.write({\n nodeId,\n attributeId: AttributeIds.Value,\n value: { value: { dataType: DataType.String, value } },\n });\n if (!status.isGood()) throw new Error(`OPC UA service write failed: ${status.toString()}`);\n };\n const read = async (session: ClientSession, paths: readonly string[]) => {\n const requests = paths.map((path) => {\n const nodeId = nodes.get(path);\n if (!nodeId) throw new Error(`Missing OPC UA service channel: ${path}`);\n return { nodeId, attributeId: AttributeIds.Value };\n });\n const values = await session.read(requests, 0);\n if (values.length !== requests.length || values.some((value) => !value.statusCode.isGood()))\n throw new Error(\"OPC UA service read failed\");\n return values.map((value): unknown => value.value.value);\n };\n const reply = async (\n session: ClientSession,\n channel: ServiceChannel,\n identity: string,\n body: string,\n signal: AbortSignal,\n ) => {\n const previous = writers.get(channel.reply) ?? Promise.resolve();\n const writing = previous\n .catch(() => undefined)\n .then(async () => {\n if (signal.aborted || activeSession !== session) return;\n await write(session, channel.reply, body);\n // Commit after the entire payload write finishes. PLC readers ignore an uncommitted string.\n await write(session, channel.replyId, identity);\n });\n writers.set(channel.reply, writing);\n try {\n await writing;\n } finally {\n if (writers.get(channel.reply) === writing) writers.delete(channel.reply);\n }\n };\n try {\n while (!options.signal.aborted) {\n try {\n const session = options.getSession();\n if (session !== activeSession) {\n for (const work of pending.values()) work.controller.abort();\n nodes = await discoverVariables(\n session,\n options.channels.flatMap((channel) => [\n channel.request,\n channel.requestId,\n channel.reply,\n channel.replyId,\n channel.epoch,\n ]),\n );\n epochs.clear();\n for (const channel of options.channels) {\n await write(session, channel.replyId, \"\");\n const epoch = randomBytes(16).toString(\"hex\");\n await write(session, channel.epoch, epoch);\n epochs.set(channel.request, epoch);\n }\n activeSession = session;\n handled.clear();\n options.onReady?.(session);\n }\n // A commit is sampled on both sides of its payload to reject a concurrent PLC update.\n const before = await read(\n session,\n options.channels.map((channel) => channel.requestId),\n );\n const batch = await read(\n session,\n options.channels.flatMap((channel) => [channel.request, channel.epoch]),\n );\n const after = await read(\n session,\n options.channels.map((channel) => channel.requestId),\n );\n for (const [index, channel] of options.channels.entries()) {\n const identity = after[index];\n if (typeof identity !== \"string\") throw new Error(\"Invalid OPC UA service identity type\");\n const current = pending.get(channel.request);\n if (current && current.identity !== identity) current.controller.abort();\n const request = batch[index * 2],\n epochValue = batch[index * 2 + 1];\n if (epochValue === \"\") {\n const epoch = randomBytes(16).toString(\"hex\");\n await write(session, channel.epoch, epoch);\n epochs.set(channel.request, epoch);\n continue;\n }\n if (before[index] !== identity || !identity) continue;\n const epoch = epochs.get(channel.request);\n if (epochValue !== epoch) throw new Error(\"OPC UA service gateway ownership changed\");\n if (\n typeof request !== \"string\" ||\n !identity.startsWith(epoch + \":\") ||\n !request.startsWith(identity + \"|\")\n )\n continue;\n if (handled.get(channel.request) === identity) continue;\n handled.set(channel.request, identity);\n const controller = new AbortController();\n const signal = AbortSignal.any([options.signal, controller.signal]);\n const operation = (async () => {\n // Uncooperative cancelled handlers still occupy bounded capacity until they settle.\n if (invocations.size >= options.channels.length * 2) {\n await reply(session, channel, identity, `${identity}|1|{}`, signal);\n return;\n }\n const invocation = dispatch(\n new Request(\"http://tsplc.internal\" + channel.path, {\n method: \"POST\",\n body: request.slice(identity.length + 1),\n signal,\n }),\n );\n invocations.add(invocation);\n void invocation.then(\n () => invocations.delete(invocation),\n () => invocations.delete(invocation),\n );\n const timer = new AbortController();\n const response = await Promise.race([\n invocation,\n delay(5000, undefined, { signal: AbortSignal.any([signal, timer.signal]) }).then(\n () => {\n throw new Error(\"OPC UA service handler timed out\");\n },\n ),\n ]).finally(() => timer.abort());\n const body = `${identity}|${response.ok ? 0 : 1}|${response.ok ? await response.text() : \"{}\"}`;\n if (Buffer.byteLength(body, \"utf8\") > 2048)\n throw new Error(\"OPC UA service reply exceeds 2048 bytes\");\n await reply(session, channel, identity, body, signal);\n })()\n .catch((error) => {\n if (!signal.aborted) options.onError(error);\n })\n .finally(() => {\n if (pending.get(channel.request)?.controller === controller)\n pending.delete(channel.request);\n });\n pending.set(channel.request, { identity, controller, done: operation });\n }\n connectionError = undefined;\n } catch (error) {\n activeSession = undefined;\n for (const work of pending.values()) work.controller.abort();\n const message = error instanceof Error ? error.message : String(error);\n if (connectionError !== message) {\n connectionError = message;\n options.onError(error);\n }\n await delay(1000, undefined, { signal: options.signal });\n }\n await delay(20, undefined, { signal: options.signal });\n }\n } catch (error) {\n if (!options.signal.aborted) throw error;\n } finally {\n activeSession = undefined;\n for (const work of pending.values()) work.controller.abort();\n await Promise.all([...pending.values()].map((work) => work.done));\n await Promise.allSettled(writers.values());\n }\n};\n",
|
|
6
|
+
"import { TRPCError, type AnyRouter, type inferRouterContext } from \"@trpc/server\";\nimport { serviceContracts, servicePath, validateWireRecord } from \"./contract\";\nexport {\n iec,\n isIntegerType,\n serviceContract,\n serviceContracts,\n servicePath,\n wireLayout,\n wireRecord,\n type IntegerType,\n type PlcMeta,\n type Wire,\n type ServiceContract,\n type WireField,\n type WireRecord,\n type WireScalar,\n} from \"./contract\";\n\n/** Mount in an authenticated Fetch host; createContext must resolve the caller's identity. */\nexport const createServiceHandler = <Router extends AnyRouter>(options: {\n router: Router;\n createContext: (\n request: Request,\n ) => inferRouterContext<Router> | Promise<inferRouterContext<Router>>;\n onError?: (error: unknown) => void;\n}) => {\n const contracts = serviceContracts(options.router);\n const paths = new Map(contracts.map((contract) => [servicePath(contract), contract]));\n const response = (value: unknown, status = 200) => {\n const body = JSON.stringify(value);\n return new Response(body, {\n status,\n headers: {\n \"Content-Type\": \"application/json; charset=utf-8\",\n \"Content-Length\": String(new TextEncoder().encode(body).length),\n \"Cache-Control\": \"no-store\",\n },\n });\n };\n return async (request: Request): Promise<Response> => {\n const path = new URL(request.url).pathname;\n const contract = paths.get(path);\n if (!contract) return response({ error: \"Unknown procedure or incompatible contract\" }, 409);\n if (request.method !== \"POST\") return response({ error: \"Use POST\" }, 405);\n const length = request.headers.get(\"content-length\");\n if (length !== null && (!/^\\d+$/.test(length) || Number(length) > 2048))\n return response({ error: \"Request exceeds the PLC wire limit\" }, 413);\n const reader = request.body?.getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n if (reader)\n try {\n for (;;) {\n const chunk = await reader.read();\n if (chunk.done) break;\n size += chunk.value.length;\n if (size > 2048) {\n await reader.cancel();\n return response({ error: \"Request exceeds the PLC wire limit\" }, 413);\n }\n chunks.push(chunk.value);\n }\n } finally {\n reader.releaseLock();\n }\n const bytes = new Uint8Array(size);\n let offset = 0;\n for (const chunk of chunks) {\n bytes.set(chunk, offset);\n offset += chunk.length;\n }\n let input: unknown;\n try {\n input = JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes));\n validateWireRecord(contract.input, input);\n } catch {\n return response({ error: \"Invalid procedure input\" }, 400);\n }\n try {\n const context = await options.createContext(request);\n const caller = options.router.createCaller(context, { signal: request.signal });\n const invoke: unknown = Reflect.get(caller, contract.path);\n if (typeof invoke !== \"function\") throw new Error(\"Invalid tRPC caller.\");\n const output: unknown = await invoke(input);\n validateWireRecord(contract.output, output);\n return response(output);\n } catch (cause) {\n if (cause instanceof TRPCError) {\n const statuses: Partial<Record<TRPCError[\"code\"], number>> = {\n UNAUTHORIZED: 401,\n FORBIDDEN: 403,\n BAD_REQUEST: 400,\n NOT_FOUND: 404,\n CONFLICT: 409,\n TOO_MANY_REQUESTS: 429,\n };\n const status = statuses[cause.code];\n if (status === undefined) options.onError?.(cause);\n return response({ error: cause.code }, status ?? 500);\n }\n options.onError?.(cause);\n return response({ error: \"Procedure failed\" }, 500);\n }\n };\n};\n",
|
|
7
|
+
"import { createHash } from \"node:crypto\";\nimport type { AnyRouter, AnyProcedure } from \"@trpc/server\";\nimport { z } from \"zod\";\n\n/** Marks a tRPC procedure as callable by the PLC; the PLC calls it through `createServiceClient`. */\nexport interface PlcMeta {\n plc?: boolean;\n}\n\nexport type WireScalar =\n | { kind: \"boolean\" }\n | { kind: \"integer\"; iec: IntegerType; minimum: number; maximum: number }\n | { kind: \"string\"; capacity: number }\n | { kind: \"enum\"; members: Record<string, number> };\nexport type WireField = WireScalar | { kind: \"array\"; length: number; element: WireScalar };\nexport interface WireRecord {\n fields: Record<string, WireField>;\n}\nexport interface ServiceContract {\n path: string;\n kind: \"query\" | \"mutation\";\n input: WireRecord;\n output: WireRecord;\n version: string;\n}\n\nconst integerTypes = {\n SINT: [-128, 127],\n USINT: [0, 255],\n BYTE: [0, 255],\n INT: [-32768, 32767],\n UINT: [0, 65535],\n WORD: [0, 65535],\n DINT: [-2147483648, 2147483647],\n UDINT: [0, 4294967295],\n DWORD: [0, 4294967295],\n} as const;\nexport type IntegerType = keyof typeof integerTypes;\n\n/** The type-only property that carries a wire value's PLC layout through tRPC's inference. */\nexport const wireLayout = \"~plc\";\n/** A wire value whose type names its PLC layout, so the PLC compiler reads it without running the server. */\nexport type Wire<T, Layout> = T & { readonly [wireLayout]?: Layout };\ntype Schema<T> = z.ZodType<T, T>;\n\nconst integer =\n <const Type extends IntegerType>(type: Type) =>\n <\n const Minimum extends number = (typeof integerTypes)[Type][0],\n const Maximum extends number = (typeof integerTypes)[Type][1],\n >(\n bounds: { min?: Minimum; max?: Maximum } = {},\n ): Schema<Wire<number, { iec: Type; minimum: Minimum; maximum: Maximum }>> =>\n z\n .number()\n .int()\n .min(bounds.min ?? integerTypes[type][0])\n .max(bounds.max ?? integerTypes[type][1])\n .meta({ iec: type });\n\n/**\n * Zod schemas for PLC wire values. Booleans are `z.boolean()` and enums are `z.enum()` of a\n * numeric TypeScript enum the PLC also imports; records are `z.strictObject()` of these.\n */\nexport const iec = {\n SINT: integer(\"SINT\"),\n USINT: integer(\"USINT\"),\n BYTE: integer(\"BYTE\"),\n INT: integer(\"INT\"),\n UINT: integer(\"UINT\"),\n WORD: integer(\"WORD\"),\n DINT: integer(\"DINT\"),\n UDINT: integer(\"UDINT\"),\n DWORD: integer(\"DWORD\"),\n /** A UTF-8 string of at most `capacity` bytes. */\n STRING: <const N extends number>(capacity: N): Schema<Wire<string, { capacity: N }>> =>\n z.string().max(capacity),\n FixedArray: <T, const N extends number>(\n element: Schema<T>,\n length: N,\n ): Schema<Wire<T[], { length: N }>> => z.array(element).length(length),\n};\n\nconst identifier = /^(?!__proto__$)[A-Za-z_][A-Za-z0-9_]*$/;\nexport const isIntegerType = (value: string): value is IntegerType =>\n Object.hasOwn(integerTypes, value);\n\n/**\n * Checks a scalar's layout and rebuilds it in canonical key order, with enum members zero first,\n * so the version digest does not depend on which side built it or on declaration order.\n */\nconst wireScalar = (field: WireScalar, label: string): WireScalar => {\n switch (field.kind) {\n case \"boolean\":\n return { kind: \"boolean\" };\n case \"string\":\n if (!Number.isInteger(field.capacity) || field.capacity < 1 || field.capacity > 2048)\n throw new Error(`${label}: string capacity must be 1..2048 bytes.`);\n return { kind: \"string\", capacity: field.capacity };\n case \"integer\": {\n const [minimum, maximum] = integerTypes[field.iec];\n if (!Number.isInteger(field.minimum) || !Number.isInteger(field.maximum))\n throw new Error(`${label}: integer bounds must be integers.`);\n if (field.minimum > field.maximum)\n throw new Error(`${label}: integer bounds are contradictory.`);\n if (field.minimum < minimum || field.maximum > maximum)\n throw new Error(`${label}: integer bounds do not fit ${field.iec}.`);\n return { kind: \"integer\", iec: field.iec, minimum: field.minimum, maximum: field.maximum };\n }\n case \"enum\": {\n const values = Object.entries(field.members).sort((left, right) => {\n if (left[1] === 0) return -1;\n if (right[1] === 0) return 1;\n return left[1] - right[1];\n });\n if (\n !values.length ||\n values.some(\n ([name, value]) =>\n !identifier.test(name) ||\n !Number.isInteger(value) ||\n value < -2147483648 ||\n value > 2147483647,\n )\n )\n throw new Error(`${label}: PLC enums require named DINT values.`);\n if (new Set(values.map(([, value]) => value)).size !== values.length)\n throw new Error(`${label}: PLC enum values must be distinct.`);\n return { kind: \"enum\", members: Object.fromEntries(values) };\n }\n }\n};\n\n/** Checks a record against the native JSON codec's limits, in canonical field order. */\nexport const wireRecord = (\n fields: Record<string, WireField>,\n label: string,\n output: boolean,\n): WireRecord => {\n const checked: Record<string, WireField> = {};\n for (const key of Object.keys(fields).sort()) {\n const field = fields[key]!;\n if (!identifier.test(key)) throw new Error(`${label}.${key}: use an IEC field identifier.`);\n if (field.kind !== \"array\") {\n checked[key] = wireScalar(field, `${label}.${key}`);\n continue;\n }\n if (output)\n throw new Error(\n `${label}.${key}: the native HTTP decoder currently supports scalar response fields only.`,\n );\n if (!Number.isInteger(field.length) || field.length < 1 || field.length > 256)\n throw new Error(`${label}.${key}: arrays require one fixed length from 1 to 256.`);\n checked[key] = {\n kind: \"array\",\n length: field.length,\n element: wireScalar(field.element, `${label}.${key}`),\n };\n }\n if (Object.keys(checked).length > 31 || (output && !Object.keys(checked).length))\n throw new Error(`${label}: response records require 1..31 fields; requests allow 0..31.`);\n return { fields: checked };\n};\n\n/** One owner of the path digest, so the PLC compiler and the host agree without sharing a file. */\nexport const serviceContract = (\n path: string,\n kind: ServiceContract[\"kind\"],\n input: WireRecord,\n output: WireRecord,\n): ServiceContract => {\n if (path.split(\".\").some((part) => !identifier.test(part)))\n throw new Error(`${path}: PLC procedure paths require identifier segments.`);\n const version = createHash(\"sha256\")\n .update(JSON.stringify({ abi: 2, path, kind, input, output }))\n .digest(\"hex\")\n .slice(0, 24);\n return { path, kind, input, output, version };\n};\n\nconst schemaValue = (value: unknown): value is z.ZodType =>\n typeof value === \"object\" && value !== null && \"_zod\" in value && \"safeParse\" in value;\nconst schema = (value: unknown, label: string): z.ZodType => {\n if (!schemaValue(value))\n throw new Error(\n `${label}: provide a bounded Zod input/output schema; TypeScript number/string inference has no IEC memory layout.`,\n );\n return value;\n};\nconst jsonSchema = z.looseObject({\n type: z.string().optional(),\n maxLength: z.number().optional(),\n minItems: z.number().optional(),\n maxItems: z.number().optional(),\n minimum: z.number().optional(),\n maximum: z.number().optional(),\n iec: z.string().optional(),\n});\n/** Schema metadata from the schema's own Zod instance; JSON Schema output varies by Zod version. */\nconst metadata = (value: z.ZodType) => jsonSchema.parse(value.meta() ?? {});\nconst refSchema = z.looseObject({\n $ref: z.string().optional(),\n $defs: z.record(z.string(), z.unknown()).optional(),\n});\n/** The JSON Schema of `value` itself, following the top-level `$ref` newer Zod emits for schemas with an id. */\nconst jsonFor = (value: z.ZodType, options: Parameters<typeof z.toJSONSchema>[1]) => {\n const json = refSchema.parse(z.toJSONSchema(value, options));\n const definition = json.$ref?.startsWith(\"#/$defs/\")\n ? json.$defs?.[json.$ref.slice(\"#/$defs/\".length)]\n : json;\n return jsonSchema.parse(definition);\n};\nconst scalar = (value: z.ZodType, label: string): WireScalar => {\n const definition = value._zod.def;\n if (\n [\"optional\", \"nullable\", \"union\", \"default\", \"prefault\", \"pipe\", \"catch\"].includes(\n definition.type,\n )\n )\n throw new Error(\n `${label}: optional, nullable, transformed and defaulted wire fields are unsupported.`,\n );\n if (definition.type === \"enum\") {\n const entries = z\n .record(z.string(), z.union([z.number().int(), z.string()]))\n .parse(Reflect.get(definition, \"entries\"));\n const members: Record<string, number> = {};\n // A TypeScript numeric enum also maps each value back to its name; those entries are skipped.\n for (const [name, value] of Object.entries(entries)) {\n if (typeof value === \"number\") members[name] = value;\n else if (identifier.test(name) && entries[value] !== name)\n throw new Error(`${label}: PLC enums require only numeric members.`);\n }\n return { kind: \"enum\", members };\n }\n const json = jsonFor(value, { io: \"input\", unrepresentable: \"throw\" });\n if (json.type === \"boolean\") return { kind: \"boolean\" };\n if (json.type === \"string\" && json.maxLength !== undefined)\n return { kind: \"string\", capacity: json.maxLength };\n const type = metadata(value).iec;\n if (\n json.type === \"integer\" &&\n type !== undefined &&\n isIntegerType(type) &&\n json.minimum !== undefined &&\n json.maximum !== undefined\n )\n return { kind: \"integer\", iec: type, minimum: json.minimum, maximum: json.maximum };\n throw new Error(\n `${label}: use z.boolean(), z.enum() of a numeric enum, iec.STRING(), or an integer such as iec.INT(). Optional, nullable, union, float and dynamic layouts are unsupported.`,\n );\n};\nconst record = (declared: z.ZodType, label: string, output: boolean): WireRecord => {\n // An input record may `.transform()` into the handler's shape; the wire keeps the record.\n const piped = declared._zod.def;\n const value =\n !output && piped.type === \"pipe\" ? schema(Reflect.get(piped, \"in\"), label) : declared;\n const def = value._zod.def;\n if (def.type !== \"object\")\n throw new Error(`${label}: HTTP services require a closed object schema.`);\n const shape = z\n .record(z.string(), z.custom<z.ZodType>(schemaValue))\n .parse(Reflect.get(def, \"shape\"));\n const fields: Record<string, WireField> = {};\n for (const [key, item] of Object.entries(shape)) {\n if (item._zod.def.type !== \"array\") {\n fields[key] = scalar(item, `${label}.${key}`);\n continue;\n }\n const bounds = jsonFor(item, { io: \"input\" });\n if (bounds.maxItems === undefined || bounds.maxItems !== bounds.minItems)\n throw new Error(`${label}.${key}: arrays require one fixed length from 1 to 256.`);\n fields[key] = {\n kind: \"array\",\n length: bounds.maxItems,\n element: scalar(\n schema(Reflect.get(item._zod.def, \"element\"), `${label}.${key}`),\n `${label}.${key}`,\n ),\n };\n }\n return wireRecord(fields, label, output);\n};\n\n/** Reflection is isolated here because tRPC v11 keeps parsers on procedure._def. */\nexport const serviceContracts = (router: AnyRouter): ServiceContract[] => {\n const contracts: ServiceContract[] = [];\n const procedures = z.record(z.string(), z.unknown()).parse(router._def.procedures);\n for (const [path, procedure] of Object.entries(procedures)) {\n if (typeof procedure !== \"function\" || !(\"_def\" in procedure))\n throw new Error(\"Unsupported tRPC router metadata.\");\n const reflected = z\n .custom<AnyProcedure>((value) => typeof value === \"function\" && \"_def\" in value)\n .parse(procedure);\n const def = reflected._def;\n if (\n typeof def.meta !== \"object\" ||\n def.meta === null ||\n !(\"plc\" in def.meta) ||\n def.meta.plc !== true\n )\n continue;\n if (def.type !== \"query\" && def.type !== \"mutation\")\n throw new Error(\n `${path}: subscriptions use a different transport; expose a query or mutation to the PLC.`,\n );\n if (def.inputs.length > 1)\n throw new Error(`${path}: merge the input into one bounded object schema.`);\n const input = record(\n def.inputs.length ? schema(def.inputs[0], `${path}.input`) : z.strictObject({}),\n `${path}.Input`,\n false,\n );\n const output = record(\n schema(Reflect.get(def, \"output\"), `${path}.output`),\n `${path}.Output`,\n true,\n );\n contracts.push(serviceContract(path, def.type, input, output));\n }\n return contracts.sort((a, b) => a.path.localeCompare(b.path));\n};\n\nexport const servicePath = (contract: ServiceContract) =>\n `/tsplc/services/${contract.version}/${contract.path}`;\n\nexport const validateWireRecord = (record: WireRecord, value: unknown): void => {\n const scalarValue = (field: WireScalar, value: unknown): boolean => {\n switch (field.kind) {\n case \"boolean\":\n return typeof value === \"boolean\";\n case \"integer\":\n return (\n typeof value === \"number\" &&\n Number.isInteger(value) &&\n value >= field.minimum &&\n value <= field.maximum\n );\n case \"string\":\n return (\n typeof value === \"string\" &&\n !value.includes(\"\\0\") &&\n new TextEncoder().encode(value).length <= field.capacity\n );\n case \"enum\":\n return typeof value === \"number\" && Object.values(field.members).includes(value);\n }\n };\n if (typeof value !== \"object\" || value === null || Array.isArray(value))\n throw new Error(\"Expected a closed wire record.\");\n const entries = Object.entries(value);\n if (entries.length !== Object.keys(record.fields).length)\n throw new Error(\"Wire record fields differ from the contract.\");\n for (const [name, field] of Object.entries(record.fields)) {\n const item: unknown = Reflect.get(value, name);\n const valid =\n field.kind === \"array\"\n ? Array.isArray(item) &&\n item.length === field.length &&\n item.every((value) => scalarValue(field.element, value))\n : scalarValue(field, item);\n if (!valid) throw new Error(`Wire field ${name} violates its PLC representation.`);\n }\n if (new TextEncoder().encode(JSON.stringify(value)).length > 2048)\n throw new Error(\"Wire record exceeds the 2048-byte native JSON limit.\");\n};\n"
|
|
8
|
+
],
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "5CEF2E462C80B5EC64756E2164756E21",
|
|
11
|
+
"names": []
|
|
12
|
+
}
|