@pulse-compute/grip 0.0.0 → 1.0.0-beta.1
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 +43 -1
- package/as/README.md +10 -0
- package/as/index.as.ts +28 -0
- package/conformance/native.json +22 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +135 -0
- package/dist/index.js.map +1 -0
- package/dist/pulsewasm.d.ts +51 -0
- package/dist/pulsewasm.d.ts.map +1 -0
- package/dist/pulsewasm.js +39 -0
- package/dist/pulsewasm.js.map +1 -0
- package/grip.framing-contract.json +76 -0
- package/package.json +60 -5
- package/pulse.package.json +73 -0
- package/pulsewasm.compiler.cjs +1063 -0
- package/pulsewasm.manifest.cjs +70 -0
|
@@ -0,0 +1,1063 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const phaseName = 'grip-lowering-plan';
|
|
7
|
+
|
|
8
|
+
function loadContractsDiagnostics() {
|
|
9
|
+
try {
|
|
10
|
+
return require('@pulse-compute/wasm-contracts/diagnostics');
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
13
|
+
return require('../../wasm/packages/contracts/src/diagnostics.js');
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function loadGripContracts() {
|
|
20
|
+
try {
|
|
21
|
+
return require('@pulse-compute/wasm-contracts/grip/contracts');
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
24
|
+
return require('../../wasm/packages/contracts/src/grip/contracts.js');
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function loadCanonicalRuntimeContracts() {
|
|
31
|
+
try {
|
|
32
|
+
return require('@pulse-compute/wasm-contracts/handler/canonical-runtime');
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) {
|
|
35
|
+
return require('../../wasm/packages/contracts/src/handler/canonical-runtime.js');
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
PACKAGE_VERSION,
|
|
43
|
+
normalizeArtifact,
|
|
44
|
+
normalizeDiagnostic,
|
|
45
|
+
sourceLoc,
|
|
46
|
+
stableFileName
|
|
47
|
+
} = loadContractsDiagnostics();
|
|
48
|
+
const gripContracts = loadGripContracts();
|
|
49
|
+
const canonicalRuntimeContracts = loadCanonicalRuntimeContracts();
|
|
50
|
+
|
|
51
|
+
function clone(value) {
|
|
52
|
+
if (Array.isArray(value)) return value.map(clone);
|
|
53
|
+
if (value && typeof value === 'object') {
|
|
54
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, clone(entry)]));
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function loadTypeScript(inputs = {}) {
|
|
60
|
+
if (inputs.typescript) return inputs.typescript;
|
|
61
|
+
if (inputs.ts) return inputs.ts;
|
|
62
|
+
try {
|
|
63
|
+
return require('typescript');
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const diagnostic = normalizeDiagnostic({
|
|
66
|
+
phase: phaseName,
|
|
67
|
+
severity: 'error',
|
|
68
|
+
code: 'PULSEWASM_GRIP_TYPESCRIPT_REQUIRED',
|
|
69
|
+
message: 'GRIP lowering plan extraction requires the TypeScript parser.',
|
|
70
|
+
hint: 'Call buildGripLoweringPlan from the compiler orchestrator, or pass { typescript } explicitly.',
|
|
71
|
+
loc: { file: '<grip-lowering-plan>' }
|
|
72
|
+
});
|
|
73
|
+
const thrown = new Error(diagnostic.message);
|
|
74
|
+
thrown.diagnostics = [diagnostic];
|
|
75
|
+
throw thrown;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function unwrapExpression(ts, node) {
|
|
80
|
+
let current = node;
|
|
81
|
+
while (
|
|
82
|
+
current &&
|
|
83
|
+
(ts.isParenthesizedExpression(current) ||
|
|
84
|
+
ts.isAsExpression(current) ||
|
|
85
|
+
ts.isNonNullExpression(current) ||
|
|
86
|
+
ts.isTypeAssertionExpression(current) ||
|
|
87
|
+
(typeof ts.isSatisfiesExpression === 'function' && ts.isSatisfiesExpression(current)))
|
|
88
|
+
) {
|
|
89
|
+
current = current.expression;
|
|
90
|
+
}
|
|
91
|
+
return current;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isStringLiteralLike(ts, node) {
|
|
95
|
+
return Boolean(node && (ts.isStringLiteral(node) || node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function literalText(ts, node) {
|
|
99
|
+
const unwrapped = unwrapExpression(ts, node);
|
|
100
|
+
return isStringLiteralLike(ts, unwrapped) ? unwrapped.text : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function numericLiteralValue(ts, node) {
|
|
104
|
+
const unwrapped = unwrapExpression(ts, node);
|
|
105
|
+
if (!unwrapped) return undefined;
|
|
106
|
+
return ts.isNumericLiteral(unwrapped) ? Number(unwrapped.text) : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function booleanLiteralValue(ts, node) {
|
|
110
|
+
const unwrapped = unwrapExpression(ts, node);
|
|
111
|
+
if (!unwrapped) return undefined;
|
|
112
|
+
if (unwrapped.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
113
|
+
if (unwrapped.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function locFor(sourceFile, node) {
|
|
118
|
+
return node ? sourceLoc(sourceFile, node) : { file: sourceFile ? sourceFile.fileName : '<grip-lowering-plan>' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function rangeFor(sourceFile, node) {
|
|
122
|
+
return Object.freeze({
|
|
123
|
+
start: node && typeof node.getStart === 'function' ? node.getStart(sourceFile) : 0,
|
|
124
|
+
end: node && typeof node.getEnd === 'function' ? node.getEnd() : 0
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function placementForCall(ts, call) {
|
|
129
|
+
let current = call;
|
|
130
|
+
let parent = current.parent;
|
|
131
|
+
if (parent && ts.isAwaitExpression(parent)) {
|
|
132
|
+
current = parent;
|
|
133
|
+
parent = current.parent;
|
|
134
|
+
}
|
|
135
|
+
if (parent && ts.isVoidExpression(parent)) {
|
|
136
|
+
current = parent;
|
|
137
|
+
parent = current.parent;
|
|
138
|
+
}
|
|
139
|
+
if (parent && ts.isExpressionStatement(parent)) return 'statement';
|
|
140
|
+
if (parent && ts.isReturnStatement(parent)) return 'return';
|
|
141
|
+
if (parent && ts.isVariableDeclaration(parent) && parent.initializer === current) return 'variable';
|
|
142
|
+
if (
|
|
143
|
+
parent
|
|
144
|
+
&& ts.isPropertyAssignment(parent)
|
|
145
|
+
&& parent.initializer === current
|
|
146
|
+
&& parent.parent
|
|
147
|
+
&& ts.isObjectLiteralExpression(parent.parent)
|
|
148
|
+
&& parent.parent.parent
|
|
149
|
+
&& ts.isCallExpression(parent.parent.parent)
|
|
150
|
+
) return 'statement';
|
|
151
|
+
return 'unsupported';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function canonicalEffect(ts, sourceFile, call, input) {
|
|
155
|
+
return Object.freeze({
|
|
156
|
+
version: canonicalRuntimeContracts.CANONICAL_PACKAGE_EFFECT_VERSION,
|
|
157
|
+
contractId: gripContracts.GRIP_CONTRACT_ID,
|
|
158
|
+
package: '@pulse-compute/grip',
|
|
159
|
+
import: '@pulse-compute/grip',
|
|
160
|
+
kind: `grip.${input.operation}`,
|
|
161
|
+
providerKind: 'grip',
|
|
162
|
+
operation: input.operation,
|
|
163
|
+
capability: `grip.${input.operation}`,
|
|
164
|
+
result: input.result,
|
|
165
|
+
placement: placementForCall(ts, call),
|
|
166
|
+
range: rangeFor(sourceFile, call),
|
|
167
|
+
resource: input.resource,
|
|
168
|
+
payload: Object.freeze({ ...(input.payload || {}) }),
|
|
169
|
+
loc: locFor(sourceFile, call)
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function makeDiagnostic(sourceFile, node, code, message, hint, severity = 'error', details) {
|
|
174
|
+
return normalizeDiagnostic({
|
|
175
|
+
phase: phaseName,
|
|
176
|
+
severity,
|
|
177
|
+
code,
|
|
178
|
+
message,
|
|
179
|
+
hint,
|
|
180
|
+
details,
|
|
181
|
+
loc: locFor(sourceFile, node)
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function diagnosticHasError(diagnostics) {
|
|
186
|
+
return (diagnostics || []).some((entry) => (entry.severity || 'error') === 'error');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function propertyKey(ts, name) {
|
|
190
|
+
if (!name) return undefined;
|
|
191
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function firstPropertyInitializer(ts, objectExpression, names) {
|
|
196
|
+
if (!objectExpression || !ts.isObjectLiteralExpression(objectExpression)) return undefined;
|
|
197
|
+
const wanted = new Set(names);
|
|
198
|
+
for (const property of objectExpression.properties || []) {
|
|
199
|
+
if (!ts.isPropertyAssignment(property)) continue;
|
|
200
|
+
const key = propertyKey(ts, property.name);
|
|
201
|
+
if (wanted.has(key)) return property.initializer;
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function stringArrayPropertyValue(ts, sourceFile, objectExpression, names, diagnostics, diagnosticCode, diagnosticMessage) {
|
|
207
|
+
const initializer = firstPropertyInitializer(ts, objectExpression, names);
|
|
208
|
+
if (!initializer) return undefined;
|
|
209
|
+
const unwrapped = unwrapExpression(ts, initializer);
|
|
210
|
+
if (!unwrapped || !ts.isArrayLiteralExpression(unwrapped)) {
|
|
211
|
+
diagnostics.push(makeDiagnostic(
|
|
212
|
+
sourceFile,
|
|
213
|
+
initializer,
|
|
214
|
+
diagnosticCode,
|
|
215
|
+
diagnosticMessage,
|
|
216
|
+
'Use a static array of string literal channels in the lowerable GRIP facade.',
|
|
217
|
+
'error',
|
|
218
|
+
{ property: names[0] }
|
|
219
|
+
));
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
const out = [];
|
|
223
|
+
for (const element of unwrapped.elements || []) {
|
|
224
|
+
const value = literalText(ts, element);
|
|
225
|
+
if (value === undefined) {
|
|
226
|
+
diagnostics.push(makeDiagnostic(
|
|
227
|
+
sourceFile,
|
|
228
|
+
element,
|
|
229
|
+
diagnosticCode,
|
|
230
|
+
diagnosticMessage,
|
|
231
|
+
'Use only literal channel names in the lowerable GRIP facade.',
|
|
232
|
+
'error',
|
|
233
|
+
{ property: names[0] }
|
|
234
|
+
));
|
|
235
|
+
} else {
|
|
236
|
+
out.push(value);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function stringPropertyValue(ts, objectExpression, names) {
|
|
243
|
+
const initializer = firstPropertyInitializer(ts, objectExpression, names);
|
|
244
|
+
if (!initializer) return undefined;
|
|
245
|
+
return literalText(ts, initializer);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function numberPropertyValue(ts, objectExpression, names) {
|
|
249
|
+
const initializer = firstPropertyInitializer(ts, objectExpression, names);
|
|
250
|
+
if (!initializer) return undefined;
|
|
251
|
+
return numericLiteralValue(ts, initializer);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function booleanPropertyValue(ts, objectExpression, names) {
|
|
255
|
+
const initializer = firstPropertyInitializer(ts, objectExpression, names);
|
|
256
|
+
if (!initializer) return undefined;
|
|
257
|
+
return booleanLiteralValue(ts, initializer);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function collectFacadeBindings(ts, sourceFile, manifest) {
|
|
261
|
+
const bindings = {
|
|
262
|
+
imports: [],
|
|
263
|
+
namespaceLocals: new Map(),
|
|
264
|
+
isWebSocketLocals: new Map(),
|
|
265
|
+
subscribeLocals: new Map(),
|
|
266
|
+
handoffLocals: new Map(),
|
|
267
|
+
broadcastLocals: new Map(),
|
|
268
|
+
holdLocals: new Map(),
|
|
269
|
+
channelLocals: new Map(),
|
|
270
|
+
publishLocals: new Map()
|
|
271
|
+
};
|
|
272
|
+
const lowerableSubpath = manifest.lowerableSubpath;
|
|
273
|
+
const compatibilitySubpaths = new Set(
|
|
274
|
+
manifest.publicApi && Array.isArray(manifest.publicApi.compatibilitySubpaths)
|
|
275
|
+
? manifest.publicApi.compatibilitySubpaths.map(String)
|
|
276
|
+
: []
|
|
277
|
+
);
|
|
278
|
+
const facadeNamespace = manifest.facade && manifest.facade.namespace ? manifest.facade.namespace : 'grip';
|
|
279
|
+
|
|
280
|
+
function rememberImport(record) {
|
|
281
|
+
bindings.imports.push(record);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
for (const statement of sourceFile.statements || []) {
|
|
285
|
+
if (!ts.isImportDeclaration(statement)) continue;
|
|
286
|
+
const moduleName = statement.moduleSpecifier && literalText(ts, statement.moduleSpecifier);
|
|
287
|
+
if (moduleName !== lowerableSubpath && !compatibilitySubpaths.has(moduleName)) continue;
|
|
288
|
+
const canonicalRoot = moduleName === lowerableSubpath;
|
|
289
|
+
const clause = statement.importClause;
|
|
290
|
+
if (!clause) continue;
|
|
291
|
+
if (clause.name) {
|
|
292
|
+
bindings.namespaceLocals.set(clause.name.text, { imported: 'default', module: moduleName, canonicalRoot, loc: locFor(sourceFile, clause.name) });
|
|
293
|
+
rememberImport({ imported: 'default', local: clause.name.text, module: moduleName, canonicalRoot, loc: locFor(sourceFile, clause.name) });
|
|
294
|
+
}
|
|
295
|
+
const namedBindings = clause.namedBindings;
|
|
296
|
+
if (!namedBindings) continue;
|
|
297
|
+
if (ts.isNamespaceImport(namedBindings)) {
|
|
298
|
+
bindings.namespaceLocals.set(namedBindings.name.text, { imported: '*', module: moduleName, canonicalRoot, loc: locFor(sourceFile, namedBindings.name) });
|
|
299
|
+
rememberImport({ imported: '*', local: namedBindings.name.text, module: moduleName, canonicalRoot, loc: locFor(sourceFile, namedBindings.name) });
|
|
300
|
+
} else if (ts.isNamedImports(namedBindings)) {
|
|
301
|
+
for (const specifier of namedBindings.elements || []) {
|
|
302
|
+
const imported = specifier.propertyName ? specifier.propertyName.text : specifier.name.text;
|
|
303
|
+
const local = specifier.name.text;
|
|
304
|
+
if (imported === facadeNamespace) {
|
|
305
|
+
bindings.namespaceLocals.set(local, { imported, module: moduleName, canonicalRoot, loc: locFor(sourceFile, specifier.name) });
|
|
306
|
+
rememberImport({ imported, local, module: moduleName, canonicalRoot, loc: locFor(sourceFile, specifier.name) });
|
|
307
|
+
} else if (canonicalRoot && imported === 'isWebSocket') {
|
|
308
|
+
bindings.isWebSocketLocals.set(local, { imported, module: moduleName, canonicalRoot, loc: locFor(sourceFile, specifier.name) });
|
|
309
|
+
rememberImport({ imported, local, module: moduleName, canonicalRoot, loc: locFor(sourceFile, specifier.name) });
|
|
310
|
+
} else if (canonicalRoot && imported === 'subscribe') {
|
|
311
|
+
bindings.subscribeLocals.set(local, { imported, module: moduleName, canonicalRoot, loc: locFor(sourceFile, specifier.name) });
|
|
312
|
+
rememberImport({ imported, local, module: moduleName, canonicalRoot, loc: locFor(sourceFile, specifier.name) });
|
|
313
|
+
} else if (canonicalRoot && imported === 'handoff') {
|
|
314
|
+
bindings.handoffLocals.set(local, { imported, module: moduleName, canonicalRoot, loc: locFor(sourceFile, specifier.name) });
|
|
315
|
+
rememberImport({ imported, local, module: moduleName, canonicalRoot, loc: locFor(sourceFile, specifier.name) });
|
|
316
|
+
} else if (canonicalRoot && imported === 'broadcast') {
|
|
317
|
+
bindings.broadcastLocals.set(local, { imported, module: moduleName, canonicalRoot, loc: locFor(sourceFile, specifier.name) });
|
|
318
|
+
rememberImport({ imported, local, module: moduleName, canonicalRoot, loc: locFor(sourceFile, specifier.name) });
|
|
319
|
+
} else if (imported === 'hold') {
|
|
320
|
+
bindings.holdLocals.set(local, { imported, loc: locFor(sourceFile, specifier.name) });
|
|
321
|
+
rememberImport({ imported, local, loc: locFor(sourceFile, specifier.name) });
|
|
322
|
+
} else if (imported === 'channel') {
|
|
323
|
+
bindings.channelLocals.set(local, { imported, loc: locFor(sourceFile, specifier.name) });
|
|
324
|
+
rememberImport({ imported, local, loc: locFor(sourceFile, specifier.name) });
|
|
325
|
+
} else if (imported === 'publish') {
|
|
326
|
+
bindings.publishLocals.set(local, { imported, local, loc: locFor(sourceFile, specifier.name) });
|
|
327
|
+
rememberImport({ imported, local, loc: locFor(sourceFile, specifier.name) });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return bindings;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function resolveGripCall(ts, call, bindings) {
|
|
336
|
+
const expression = unwrapExpression(ts, call.expression);
|
|
337
|
+
if (!expression) return undefined;
|
|
338
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
339
|
+
const object = unwrapExpression(ts, expression.expression);
|
|
340
|
+
const method = expression.name.text;
|
|
341
|
+
const namespace = object && ts.isIdentifier(object) ? bindings.namespaceLocals.get(object.text) : undefined;
|
|
342
|
+
const methods = namespace && namespace.canonicalRoot
|
|
343
|
+
? ['broadcast', 'handoff', 'isWebSocket', 'subscribe']
|
|
344
|
+
: ['hold', 'channel', 'publish'];
|
|
345
|
+
if (namespace && methods.includes(method)) {
|
|
346
|
+
return { symbol: `grip.${method}`, method, local: object.text, canonicalRoot: namespace.canonicalRoot };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (ts.isIdentifier(expression)) {
|
|
350
|
+
if (bindings.isWebSocketLocals.has(expression.text)) return { symbol: 'grip.isWebSocket', method: 'isWebSocket', local: expression.text, canonicalRoot: true };
|
|
351
|
+
if (bindings.subscribeLocals.has(expression.text)) return { symbol: 'grip.subscribe', method: 'subscribe', local: expression.text, canonicalRoot: true };
|
|
352
|
+
if (bindings.handoffLocals.has(expression.text)) return { symbol: 'grip.handoff', method: 'handoff', local: expression.text, canonicalRoot: true };
|
|
353
|
+
if (bindings.broadcastLocals.has(expression.text)) return { symbol: 'grip.broadcast', method: 'broadcast', local: expression.text, canonicalRoot: true };
|
|
354
|
+
if (bindings.holdLocals.has(expression.text)) return { symbol: 'grip.hold', method: 'hold', local: expression.text };
|
|
355
|
+
if (bindings.channelLocals.has(expression.text)) return { symbol: 'grip.channel', method: 'channel', local: expression.text };
|
|
356
|
+
if (bindings.publishLocals.has(expression.text)) return { symbol: 'grip.publish', method: 'publish', local: expression.text };
|
|
357
|
+
}
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function objectArg(ts, call, index) {
|
|
362
|
+
const candidate = unwrapExpression(ts, (call.arguments || [])[index]);
|
|
363
|
+
return candidate && ts.isObjectLiteralExpression(candidate) ? candidate : undefined;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function holdEntry(ts, sourceFile, call, diagnostics) {
|
|
367
|
+
const modeArg = (call.arguments || [])[0];
|
|
368
|
+
const mode = literalText(ts, modeArg);
|
|
369
|
+
if (mode === undefined) {
|
|
370
|
+
diagnostics.push(makeDiagnostic(
|
|
371
|
+
sourceFile,
|
|
372
|
+
modeArg || call,
|
|
373
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.NON_LITERAL_HOLD_MODE,
|
|
374
|
+
'grip.hold(...) requires a literal hold mode.',
|
|
375
|
+
'Use grip.hold("stream") or grip.hold("response") in the lowerable GRIP facade.',
|
|
376
|
+
'error'
|
|
377
|
+
));
|
|
378
|
+
} else if (!gripContracts.GRIP_ALLOWED_HOLD_MODES.includes(mode)) {
|
|
379
|
+
diagnostics.push(makeDiagnostic(
|
|
380
|
+
sourceFile,
|
|
381
|
+
modeArg,
|
|
382
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.HOLD_MODE_UNSUPPORTED,
|
|
383
|
+
`Unsupported GRIP hold mode ${JSON.stringify(mode)}.`,
|
|
384
|
+
`Use one of: ${gripContracts.GRIP_ALLOWED_HOLD_MODES.join(', ')}.`,
|
|
385
|
+
'error',
|
|
386
|
+
{ mode }
|
|
387
|
+
));
|
|
388
|
+
}
|
|
389
|
+
const options = objectArg(ts, call, 1);
|
|
390
|
+
const channels = stringArrayPropertyValue(
|
|
391
|
+
ts,
|
|
392
|
+
sourceFile,
|
|
393
|
+
options,
|
|
394
|
+
['channels'],
|
|
395
|
+
diagnostics,
|
|
396
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.DYNAMIC_CHANNEL_LIST_UNSUPPORTED,
|
|
397
|
+
'grip.hold(...) channels must be a static array of literal channel names.'
|
|
398
|
+
);
|
|
399
|
+
const timeoutMs = numberPropertyValue(ts, options, ['timeoutMs', 'timeout']);
|
|
400
|
+
const headersConfigured = Boolean(options && firstPropertyInitializer(ts, options, ['headers']));
|
|
401
|
+
const entry = {
|
|
402
|
+
kind: 'grip-hold',
|
|
403
|
+
symbol: 'grip.hold',
|
|
404
|
+
mode,
|
|
405
|
+
channels: channels || [],
|
|
406
|
+
timeoutMs,
|
|
407
|
+
headersConfigured,
|
|
408
|
+
asSymbol: 'pulse_grip_hold',
|
|
409
|
+
hostCapabilities: ['headers'],
|
|
410
|
+
loc: locFor(sourceFile, call),
|
|
411
|
+
range: rangeFor(sourceFile, call),
|
|
412
|
+
placement: placementForCall(ts, call)
|
|
413
|
+
};
|
|
414
|
+
entry.canonicalEffect = canonicalEffect(ts, sourceFile, call, {
|
|
415
|
+
import: '@pulse-compute/grip/pulsewasm',
|
|
416
|
+
operation: 'hold',
|
|
417
|
+
result: 'opaque-response',
|
|
418
|
+
resource: Object.freeze({ kind: 'literal', value: mode || 'stream' }),
|
|
419
|
+
payload: {
|
|
420
|
+
mode,
|
|
421
|
+
channels: channels || [],
|
|
422
|
+
timeoutMs,
|
|
423
|
+
headersConfigured
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
return entry;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function channelEntry(ts, sourceFile, call, diagnostics) {
|
|
430
|
+
const channelArg = (call.arguments || [])[0];
|
|
431
|
+
const channelName = literalText(ts, channelArg);
|
|
432
|
+
if (channelName === undefined) {
|
|
433
|
+
diagnostics.push(makeDiagnostic(
|
|
434
|
+
sourceFile,
|
|
435
|
+
channelArg || call,
|
|
436
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.NON_LITERAL_CHANNEL,
|
|
437
|
+
'grip.channel(...) requires a literal channel name.',
|
|
438
|
+
'Use a static channel string in the lowerable GRIP facade. Dynamic channel expressions remain out of scope for the Beta.',
|
|
439
|
+
'error'
|
|
440
|
+
));
|
|
441
|
+
}
|
|
442
|
+
const options = objectArg(ts, call, 1);
|
|
443
|
+
const prefix = stringPropertyValue(ts, options, ['prefix']);
|
|
444
|
+
const fanout = booleanPropertyValue(ts, options, ['fanout']);
|
|
445
|
+
const entry = {
|
|
446
|
+
kind: 'grip-channel',
|
|
447
|
+
symbol: 'grip.channel',
|
|
448
|
+
channel: channelName,
|
|
449
|
+
prefix,
|
|
450
|
+
fanout,
|
|
451
|
+
asSymbol: 'pulse_grip_channel',
|
|
452
|
+
hostCapabilities: ['headers', 'channel', 'broadcaster'],
|
|
453
|
+
loc: locFor(sourceFile, call),
|
|
454
|
+
range: rangeFor(sourceFile, call),
|
|
455
|
+
placement: placementForCall(ts, call)
|
|
456
|
+
};
|
|
457
|
+
entry.canonicalEffect = canonicalEffect(ts, sourceFile, call, {
|
|
458
|
+
import: '@pulse-compute/grip/pulsewasm',
|
|
459
|
+
operation: 'channel',
|
|
460
|
+
result: 'ack',
|
|
461
|
+
resource: Object.freeze({ kind: 'literal', value: channelName }),
|
|
462
|
+
payload: { channel: channelName, prefix, fanout }
|
|
463
|
+
});
|
|
464
|
+
return entry;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function publishEntry(ts, sourceFile, call, diagnostics) {
|
|
468
|
+
const channelArg = (call.arguments || [])[0];
|
|
469
|
+
const messageArg = (call.arguments || [])[1];
|
|
470
|
+
const channelName = literalText(ts, channelArg);
|
|
471
|
+
const message = literalText(ts, messageArg);
|
|
472
|
+
if (channelName === undefined) {
|
|
473
|
+
diagnostics.push(makeDiagnostic(
|
|
474
|
+
sourceFile,
|
|
475
|
+
channelArg || call,
|
|
476
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.NON_LITERAL_CHANNEL,
|
|
477
|
+
'grip.publish(...) requires a literal channel name.',
|
|
478
|
+
'Use a static channel string in the lowerable GRIP facade. Dynamic channel expressions remain out of scope for the Beta.',
|
|
479
|
+
'error'
|
|
480
|
+
));
|
|
481
|
+
}
|
|
482
|
+
if (message === undefined) {
|
|
483
|
+
diagnostics.push(makeDiagnostic(
|
|
484
|
+
sourceFile,
|
|
485
|
+
messageArg || call,
|
|
486
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.NON_LITERAL_MESSAGE,
|
|
487
|
+
'grip.publish(...) requires a literal message payload in the Beta.',
|
|
488
|
+
'Use a static string message for the Beta lowerable contract. Schema-backed or dynamic publish payloads are future work.',
|
|
489
|
+
'error'
|
|
490
|
+
));
|
|
491
|
+
}
|
|
492
|
+
const options = objectArg(ts, call, 2);
|
|
493
|
+
const entry = {
|
|
494
|
+
kind: 'grip-publish',
|
|
495
|
+
symbol: 'grip.publish',
|
|
496
|
+
channel: channelName,
|
|
497
|
+
message,
|
|
498
|
+
event: stringPropertyValue(ts, options, ['event', 'type']),
|
|
499
|
+
id: stringPropertyValue(ts, options, ['id']),
|
|
500
|
+
asSymbol: 'pulse_grip_publish',
|
|
501
|
+
hostCapabilities: ['broadcaster', 'clock'],
|
|
502
|
+
loc: locFor(sourceFile, call),
|
|
503
|
+
range: rangeFor(sourceFile, call),
|
|
504
|
+
placement: placementForCall(ts, call)
|
|
505
|
+
};
|
|
506
|
+
entry.canonicalEffect = canonicalEffect(ts, sourceFile, call, {
|
|
507
|
+
import: '@pulse-compute/grip/pulsewasm',
|
|
508
|
+
operation: 'publish',
|
|
509
|
+
result: 'structured-response',
|
|
510
|
+
resource: Object.freeze({ kind: 'literal', value: channelName }),
|
|
511
|
+
payload: {
|
|
512
|
+
channel: channelName,
|
|
513
|
+
message,
|
|
514
|
+
event: entry.event,
|
|
515
|
+
id: entry.id
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
return entry;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function enclosingContextName(ts, node) {
|
|
522
|
+
let current = node && node.parent;
|
|
523
|
+
while (current) {
|
|
524
|
+
if (
|
|
525
|
+
(ts.isFunctionDeclaration(current)
|
|
526
|
+
|| ts.isFunctionExpression(current)
|
|
527
|
+
|| ts.isArrowFunction(current)
|
|
528
|
+
|| ts.isMethodDeclaration(current))
|
|
529
|
+
&& current.parameters
|
|
530
|
+
&& current.parameters[0]
|
|
531
|
+
&& ts.isIdentifier(current.parameters[0].name)
|
|
532
|
+
) return current.parameters[0].name.text;
|
|
533
|
+
current = current.parent;
|
|
534
|
+
}
|
|
535
|
+
return 'ctx';
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function staticJsonValue(ts, sourceFile, node, diagnostics, diagnosticCode, message) {
|
|
539
|
+
const current = unwrapExpression(ts, node);
|
|
540
|
+
if (!current) return undefined;
|
|
541
|
+
if (isStringLiteralLike(ts, current)) return current.text;
|
|
542
|
+
if (ts.isNumericLiteral(current)) return Number(current.text);
|
|
543
|
+
if (current.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
544
|
+
if (current.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
545
|
+
if (current.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
546
|
+
if (
|
|
547
|
+
ts.isPrefixUnaryExpression(current)
|
|
548
|
+
&& current.operator === ts.SyntaxKind.MinusToken
|
|
549
|
+
&& ts.isNumericLiteral(current.operand)
|
|
550
|
+
) return -Number(current.operand.text);
|
|
551
|
+
if (ts.isArrayLiteralExpression(current)) {
|
|
552
|
+
const output = [];
|
|
553
|
+
for (const element of current.elements || []) {
|
|
554
|
+
if (ts.isSpreadElement(element) || ts.isOmittedExpression(element)) {
|
|
555
|
+
diagnostics.push(makeDiagnostic(
|
|
556
|
+
sourceFile,
|
|
557
|
+
element,
|
|
558
|
+
diagnosticCode,
|
|
559
|
+
message,
|
|
560
|
+
'Use JSON-literal arrays without spreads or omitted elements.',
|
|
561
|
+
'error'
|
|
562
|
+
));
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
output.push(staticJsonValue(ts, sourceFile, element, diagnostics, diagnosticCode, message));
|
|
566
|
+
}
|
|
567
|
+
return output;
|
|
568
|
+
}
|
|
569
|
+
if (ts.isObjectLiteralExpression(current)) {
|
|
570
|
+
const output = {};
|
|
571
|
+
const seen = new Set();
|
|
572
|
+
for (const property of current.properties || []) {
|
|
573
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
574
|
+
diagnostics.push(makeDiagnostic(
|
|
575
|
+
sourceFile,
|
|
576
|
+
property,
|
|
577
|
+
diagnosticCode,
|
|
578
|
+
message,
|
|
579
|
+
'Use an ordinary JSON-literal object without spreads, shorthand properties, methods, getters, or setters.',
|
|
580
|
+
'error'
|
|
581
|
+
));
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
const key = propertyKey(ts, property.name);
|
|
585
|
+
if (key === undefined || seen.has(key) || key === '__proto__') {
|
|
586
|
+
diagnostics.push(makeDiagnostic(
|
|
587
|
+
sourceFile,
|
|
588
|
+
property.name,
|
|
589
|
+
diagnosticCode,
|
|
590
|
+
message,
|
|
591
|
+
'Use unique static object keys; computed, duplicate, and __proto__ keys are not lowerable.',
|
|
592
|
+
'error'
|
|
593
|
+
));
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
seen.add(key);
|
|
597
|
+
output[key] = staticJsonValue(ts, sourceFile, property.initializer, diagnostics, diagnosticCode, message);
|
|
598
|
+
}
|
|
599
|
+
return output;
|
|
600
|
+
}
|
|
601
|
+
diagnostics.push(makeDiagnostic(
|
|
602
|
+
sourceFile,
|
|
603
|
+
current,
|
|
604
|
+
diagnosticCode,
|
|
605
|
+
message,
|
|
606
|
+
'Use only JSON-literal values in the bounded Native GRIP surface.',
|
|
607
|
+
'error',
|
|
608
|
+
{ expression: current.getText(sourceFile) }
|
|
609
|
+
));
|
|
610
|
+
return undefined;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function staticObjectArgument(ts, sourceFile, call, index, diagnostics, diagnosticCode, message) {
|
|
614
|
+
const argument = call.arguments && call.arguments[index];
|
|
615
|
+
const current = unwrapExpression(ts, argument);
|
|
616
|
+
if (!current || !ts.isObjectLiteralExpression(current)) {
|
|
617
|
+
diagnostics.push(makeDiagnostic(
|
|
618
|
+
sourceFile,
|
|
619
|
+
argument || call,
|
|
620
|
+
diagnosticCode,
|
|
621
|
+
message,
|
|
622
|
+
'Use an inline static object literal in the bounded Native GRIP surface.',
|
|
623
|
+
'error'
|
|
624
|
+
));
|
|
625
|
+
return {};
|
|
626
|
+
}
|
|
627
|
+
return staticJsonValue(ts, sourceFile, current, diagnostics, diagnosticCode, message) || {};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function normalizeStaticChannels(sourceFile, node, options, diagnostics) {
|
|
631
|
+
const input = options.channels !== undefined
|
|
632
|
+
? options.channels
|
|
633
|
+
: (options.channel === undefined ? [] : [options.channel]);
|
|
634
|
+
if (!Array.isArray(input) || input.length === 0) {
|
|
635
|
+
diagnostics.push(makeDiagnostic(
|
|
636
|
+
sourceFile,
|
|
637
|
+
node,
|
|
638
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.CHANNEL_INVALID,
|
|
639
|
+
'GRIP framing requires at least one static channel.',
|
|
640
|
+
'Use channel: "name" or channels: ["one", "two"].',
|
|
641
|
+
'error'
|
|
642
|
+
));
|
|
643
|
+
return [];
|
|
644
|
+
}
|
|
645
|
+
return input.map((value) => {
|
|
646
|
+
const channel = typeof value === 'string' ? value.trim() : '';
|
|
647
|
+
if (!channel || /[,\r\n]/.test(channel)) {
|
|
648
|
+
diagnostics.push(makeDiagnostic(
|
|
649
|
+
sourceFile,
|
|
650
|
+
node,
|
|
651
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.CHANNEL_INVALID,
|
|
652
|
+
'GRIP channel names must be non-empty static strings without commas or line breaks.',
|
|
653
|
+
'Use one literal channel per ordered channel entry.',
|
|
654
|
+
'error',
|
|
655
|
+
{ channel: typeof value === 'string' ? value : undefined }
|
|
656
|
+
));
|
|
657
|
+
}
|
|
658
|
+
return channel;
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function validateFramingOptions(sourceFile, node, options, diagnostics, kind) {
|
|
663
|
+
normalizeStaticChannels(sourceFile, node, options, diagnostics);
|
|
664
|
+
if (options.mode !== undefined && !gripContracts.GRIP_ALLOWED_HOLD_MODES.includes(options.mode)) {
|
|
665
|
+
diagnostics.push(makeDiagnostic(
|
|
666
|
+
sourceFile,
|
|
667
|
+
node,
|
|
668
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.MODE_UNSUPPORTED,
|
|
669
|
+
`Unsupported GRIP hold mode ${JSON.stringify(options.mode)}.`,
|
|
670
|
+
'Use "stream" or "response".',
|
|
671
|
+
'error',
|
|
672
|
+
{ mode: options.mode }
|
|
673
|
+
));
|
|
674
|
+
}
|
|
675
|
+
if (
|
|
676
|
+
options.timeoutMs !== undefined
|
|
677
|
+
&& (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs < 0)
|
|
678
|
+
) {
|
|
679
|
+
diagnostics.push(makeDiagnostic(
|
|
680
|
+
sourceFile,
|
|
681
|
+
node,
|
|
682
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.TIMEOUT_INVALID,
|
|
683
|
+
'GRIP timeoutMs must be a non-negative safe integer.',
|
|
684
|
+
'Use zero or a positive integer no larger than Number.MAX_SAFE_INTEGER.',
|
|
685
|
+
'error',
|
|
686
|
+
{ timeoutMs: options.timeoutMs }
|
|
687
|
+
));
|
|
688
|
+
}
|
|
689
|
+
if (
|
|
690
|
+
kind === 'handoff'
|
|
691
|
+
&& options.status !== undefined
|
|
692
|
+
&& (!Number.isSafeInteger(options.status) || options.status < 200 || options.status > 599)
|
|
693
|
+
) {
|
|
694
|
+
diagnostics.push(makeDiagnostic(
|
|
695
|
+
sourceFile,
|
|
696
|
+
node,
|
|
697
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.STATIC_OPTIONS_REQUIRED,
|
|
698
|
+
'GRIP handoff status must be a static integer from 200 through 599.',
|
|
699
|
+
'Use a valid Web Response status.',
|
|
700
|
+
'error',
|
|
701
|
+
{ status: options.status }
|
|
702
|
+
));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function packageIntrinsic(sourceFile, call, operation) {
|
|
707
|
+
const descriptor = gripContracts.GRIP_PACKAGE_INTRINSICS[operation];
|
|
708
|
+
return Object.freeze({
|
|
709
|
+
version: gripContracts.GRIP_PACKAGE_INTRINSIC_VERSION,
|
|
710
|
+
contractId: gripContracts.GRIP_CONTRACT_ID,
|
|
711
|
+
package: '@pulse-compute/grip',
|
|
712
|
+
import: '@pulse-compute/grip',
|
|
713
|
+
kind: `grip.${operation}`,
|
|
714
|
+
operation,
|
|
715
|
+
intrinsic: descriptor.name,
|
|
716
|
+
compilerName: descriptor.compilerName,
|
|
717
|
+
valueKind: descriptor.valueKind,
|
|
718
|
+
argumentIndexes: descriptor.argumentIndexes,
|
|
719
|
+
range: rangeFor(sourceFile, call),
|
|
720
|
+
loc: locFor(sourceFile, call)
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function isWebSocketEntry(ts, sourceFile, call, diagnostics) {
|
|
725
|
+
const argument = unwrapExpression(ts, call.arguments && call.arguments[0]);
|
|
726
|
+
const contextName = enclosingContextName(ts, call);
|
|
727
|
+
const valid = call.arguments.length === 1
|
|
728
|
+
&& argument
|
|
729
|
+
&& ts.isPropertyAccessExpression(argument)
|
|
730
|
+
&& argument.name.text === 'req'
|
|
731
|
+
&& ts.isIdentifier(unwrapExpression(ts, argument.expression))
|
|
732
|
+
&& unwrapExpression(ts, argument.expression).text === contextName;
|
|
733
|
+
if (!valid) {
|
|
734
|
+
diagnostics.push(makeDiagnostic(
|
|
735
|
+
sourceFile,
|
|
736
|
+
argument || call,
|
|
737
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.REQUEST_EXPRESSION_UNSUPPORTED,
|
|
738
|
+
'grip.isWebSocket(...) requires the current canonical handler request.',
|
|
739
|
+
`Call grip.isWebSocket(${contextName}.req) directly.`,
|
|
740
|
+
'error'
|
|
741
|
+
));
|
|
742
|
+
}
|
|
743
|
+
return Object.freeze({
|
|
744
|
+
kind: 'grip-is-websocket',
|
|
745
|
+
symbol: 'grip.isWebSocket',
|
|
746
|
+
loc: locFor(sourceFile, call),
|
|
747
|
+
range: rangeFor(sourceFile, call),
|
|
748
|
+
placement: placementForCall(ts, call),
|
|
749
|
+
canonicalIntrinsic: packageIntrinsic(sourceFile, call, 'isWebSocket')
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function subscribeEntry(ts, sourceFile, call, diagnostics) {
|
|
754
|
+
if (call.arguments.length !== 2) {
|
|
755
|
+
diagnostics.push(makeDiagnostic(
|
|
756
|
+
sourceFile,
|
|
757
|
+
call,
|
|
758
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.ARGUMENT_SHAPE_UNSUPPORTED,
|
|
759
|
+
'grip.subscribe(...) requires a response and one static options object.',
|
|
760
|
+
'Call grip.subscribe(response, { channel, mode, timeoutMs }).',
|
|
761
|
+
'error'
|
|
762
|
+
));
|
|
763
|
+
}
|
|
764
|
+
const options = staticObjectArgument(
|
|
765
|
+
ts,
|
|
766
|
+
sourceFile,
|
|
767
|
+
call,
|
|
768
|
+
1,
|
|
769
|
+
diagnostics,
|
|
770
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.STATIC_OPTIONS_REQUIRED,
|
|
771
|
+
'grip.subscribe options must be a static object.'
|
|
772
|
+
);
|
|
773
|
+
validateFramingOptions(sourceFile, call.arguments[1] || call, options, diagnostics, 'subscribe');
|
|
774
|
+
return Object.freeze({
|
|
775
|
+
kind: 'grip-subscribe',
|
|
776
|
+
symbol: 'grip.subscribe',
|
|
777
|
+
options: clone(options),
|
|
778
|
+
loc: locFor(sourceFile, call),
|
|
779
|
+
range: rangeFor(sourceFile, call),
|
|
780
|
+
placement: placementForCall(ts, call),
|
|
781
|
+
canonicalIntrinsic: packageIntrinsic(sourceFile, call, 'subscribe')
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function handoffEntry(ts, sourceFile, call, diagnostics) {
|
|
786
|
+
if (call.arguments.length !== 1) {
|
|
787
|
+
diagnostics.push(makeDiagnostic(
|
|
788
|
+
sourceFile,
|
|
789
|
+
call,
|
|
790
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.ARGUMENT_SHAPE_UNSUPPORTED,
|
|
791
|
+
'grip.handoff(...) requires one static options object.',
|
|
792
|
+
'Call grip.handoff({ channel, status, headers, body }).',
|
|
793
|
+
'error'
|
|
794
|
+
));
|
|
795
|
+
}
|
|
796
|
+
const options = staticObjectArgument(
|
|
797
|
+
ts,
|
|
798
|
+
sourceFile,
|
|
799
|
+
call,
|
|
800
|
+
0,
|
|
801
|
+
diagnostics,
|
|
802
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.STATIC_OPTIONS_REQUIRED,
|
|
803
|
+
'grip.handoff options must be a static object.'
|
|
804
|
+
);
|
|
805
|
+
validateFramingOptions(sourceFile, call.arguments[0] || call, options, diagnostics, 'handoff');
|
|
806
|
+
return Object.freeze({
|
|
807
|
+
kind: 'grip-handoff',
|
|
808
|
+
symbol: 'grip.handoff',
|
|
809
|
+
options: clone(options),
|
|
810
|
+
loc: locFor(sourceFile, call),
|
|
811
|
+
range: rangeFor(sourceFile, call),
|
|
812
|
+
placement: placementForCall(ts, call),
|
|
813
|
+
canonicalIntrinsic: packageIntrinsic(sourceFile, call, 'handoff')
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function broadcastEntry(ts, sourceFile, call, diagnostics) {
|
|
818
|
+
const contextName = enclosingContextName(ts, call);
|
|
819
|
+
const contextArg = unwrapExpression(ts, call.arguments && call.arguments[0]);
|
|
820
|
+
if (
|
|
821
|
+
call.arguments.length !== 2
|
|
822
|
+
|| !contextArg
|
|
823
|
+
|| !ts.isIdentifier(contextArg)
|
|
824
|
+
|| contextArg.text !== contextName
|
|
825
|
+
) {
|
|
826
|
+
diagnostics.push(makeDiagnostic(
|
|
827
|
+
sourceFile,
|
|
828
|
+
contextArg || call,
|
|
829
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.ARGUMENT_SHAPE_UNSUPPORTED,
|
|
830
|
+
'grip.broadcast(...) requires the current handler context and one static message.',
|
|
831
|
+
`Call grip.broadcast(${contextName}, { channel, data, event, id }).`,
|
|
832
|
+
'error'
|
|
833
|
+
));
|
|
834
|
+
}
|
|
835
|
+
const message = staticObjectArgument(
|
|
836
|
+
ts,
|
|
837
|
+
sourceFile,
|
|
838
|
+
call,
|
|
839
|
+
1,
|
|
840
|
+
diagnostics,
|
|
841
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.STATIC_MESSAGE_REQUIRED,
|
|
842
|
+
'grip.broadcast message must be a static JSON-literal object.'
|
|
843
|
+
);
|
|
844
|
+
const channel = typeof message.channel === 'string' ? message.channel.trim() : '';
|
|
845
|
+
if (!channel || /[,\r\n]/.test(channel)) {
|
|
846
|
+
diagnostics.push(makeDiagnostic(
|
|
847
|
+
sourceFile,
|
|
848
|
+
call.arguments[1] || call,
|
|
849
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.CHANNEL_INVALID,
|
|
850
|
+
'grip.broadcast channel must be a non-empty static string without commas or line breaks.',
|
|
851
|
+
'Use one literal provider channel.',
|
|
852
|
+
'error'
|
|
853
|
+
));
|
|
854
|
+
}
|
|
855
|
+
if (!Object.prototype.hasOwnProperty.call(message, 'data')) {
|
|
856
|
+
diagnostics.push(makeDiagnostic(
|
|
857
|
+
sourceFile,
|
|
858
|
+
call.arguments[1] || call,
|
|
859
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.STATIC_MESSAGE_REQUIRED,
|
|
860
|
+
'grip.broadcast message requires a static data property.',
|
|
861
|
+
'Include data with a JSON-literal value.',
|
|
862
|
+
'error'
|
|
863
|
+
));
|
|
864
|
+
}
|
|
865
|
+
const payload = {
|
|
866
|
+
channel,
|
|
867
|
+
data: message.data,
|
|
868
|
+
...(message.event === undefined ? {} : { event: String(message.event) }),
|
|
869
|
+
...(message.id === undefined ? {} : { id: String(message.id) })
|
|
870
|
+
};
|
|
871
|
+
return Object.freeze({
|
|
872
|
+
kind: 'grip-broadcast',
|
|
873
|
+
symbol: 'grip.broadcast',
|
|
874
|
+
message: clone(payload),
|
|
875
|
+
loc: locFor(sourceFile, call),
|
|
876
|
+
range: rangeFor(sourceFile, call),
|
|
877
|
+
placement: placementForCall(ts, call),
|
|
878
|
+
canonicalEffect: canonicalEffect(ts, sourceFile, call, {
|
|
879
|
+
import: '@pulse-compute/grip',
|
|
880
|
+
operation: 'broadcast',
|
|
881
|
+
result: 'ack',
|
|
882
|
+
resource: Object.freeze({ kind: 'literal', value: channel }),
|
|
883
|
+
payload
|
|
884
|
+
})
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function sourceFileFromInputs(ts, inputs) {
|
|
889
|
+
if (inputs.sourceFile) return inputs.sourceFile;
|
|
890
|
+
const sourcePath = inputs.sourcePath || inputs.entry || inputs.appPath || inputs.file;
|
|
891
|
+
const sourceText = inputs.sourceText !== undefined
|
|
892
|
+
? String(inputs.sourceText)
|
|
893
|
+
: fs.readFileSync(sourcePath, 'utf8');
|
|
894
|
+
const fileName = sourcePath || '<grip-lowering-plan>';
|
|
895
|
+
return ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function buildGripLoweringPlan(inputs = {}) {
|
|
899
|
+
const ts = loadTypeScript(inputs);
|
|
900
|
+
const sourceFile = sourceFileFromInputs(ts, inputs);
|
|
901
|
+
const manifest = inputs.gripManifest || inputs.manifest || require('./pulsewasm.manifest.cjs');
|
|
902
|
+
const diagnostics = [];
|
|
903
|
+
const bindings = collectFacadeBindings(ts, sourceFile, manifest);
|
|
904
|
+
const entries = [];
|
|
905
|
+
|
|
906
|
+
function visit(node) {
|
|
907
|
+
if (ts.isCallExpression(node)) {
|
|
908
|
+
const resolved = resolveGripCall(ts, node, bindings);
|
|
909
|
+
if (resolved) {
|
|
910
|
+
if (resolved.method === 'isWebSocket') entries.push(isWebSocketEntry(ts, sourceFile, node, diagnostics));
|
|
911
|
+
if (resolved.method === 'subscribe') entries.push(subscribeEntry(ts, sourceFile, node, diagnostics));
|
|
912
|
+
if (resolved.method === 'handoff') entries.push(handoffEntry(ts, sourceFile, node, diagnostics));
|
|
913
|
+
if (resolved.method === 'broadcast') entries.push(broadcastEntry(ts, sourceFile, node, diagnostics));
|
|
914
|
+
if (resolved.method === 'hold') entries.push(holdEntry(ts, sourceFile, node, diagnostics));
|
|
915
|
+
if (resolved.method === 'channel') entries.push(channelEntry(ts, sourceFile, node, diagnostics));
|
|
916
|
+
if (resolved.method === 'publish') entries.push(publishEntry(ts, sourceFile, node, diagnostics));
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
ts.forEachChild(node, visit);
|
|
920
|
+
}
|
|
921
|
+
visit(sourceFile);
|
|
922
|
+
|
|
923
|
+
for (const entry of entries) {
|
|
924
|
+
if (entry.canonicalIntrinsic) continue;
|
|
925
|
+
if (entry.placement !== 'statement' && entry.placement !== 'return') {
|
|
926
|
+
if (entry.symbol === 'grip.broadcast' && entry.placement === 'variable') continue;
|
|
927
|
+
diagnostics.push(makeDiagnostic(
|
|
928
|
+
sourceFile,
|
|
929
|
+
sourceFile,
|
|
930
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.HANDLE_ESCAPE_UNSUPPORTED,
|
|
931
|
+
`${entry.symbol} must be directly awaited, returned, or used as a keyed ctx.parallel member.`,
|
|
932
|
+
'Keep GRIP values inside the package-owned lowering boundary; passing them through user state remains unsupported.',
|
|
933
|
+
'error',
|
|
934
|
+
{ symbol: entry.symbol, placement: entry.placement, range: entry.range }
|
|
935
|
+
));
|
|
936
|
+
}
|
|
937
|
+
if (entry.symbol === 'grip.hold' && entry.placement !== 'return') {
|
|
938
|
+
diagnostics.push(makeDiagnostic(
|
|
939
|
+
sourceFile,
|
|
940
|
+
sourceFile,
|
|
941
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.HANDLE_ESCAPE_UNSUPPORTED,
|
|
942
|
+
'grip.hold(...) must be returned directly so the opaque response stays host-owned.',
|
|
943
|
+
'Return grip.hold(...) from the handler branch without inspecting or assigning the result.',
|
|
944
|
+
'error',
|
|
945
|
+
{ placement: entry.placement, range: entry.range }
|
|
946
|
+
));
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (bindings.imports.length > 0 && entries.length === 0) {
|
|
951
|
+
diagnostics.push(makeDiagnostic(
|
|
952
|
+
sourceFile,
|
|
953
|
+
sourceFile,
|
|
954
|
+
gripContracts.GRIP_DIAGNOSTIC_CODES.HANDLE_ESCAPE_UNSUPPORTED,
|
|
955
|
+
'A GRIP facade was imported but no supported package-owned calls were found.',
|
|
956
|
+
'Use the bounded canonical root or the retained static compatibility facade directly in handlers.',
|
|
957
|
+
'warning'
|
|
958
|
+
));
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const packageCompilerBuilder = inputs.packageCompilerBuilder || {
|
|
962
|
+
owner: manifest.compiler && manifest.compiler.builderOwner,
|
|
963
|
+
export: manifest.compiler && manifest.compiler.export,
|
|
964
|
+
trust: manifest.compiler && manifest.compiler.trust
|
|
965
|
+
};
|
|
966
|
+
const lowerings = manifest.modes && manifest.modes.wasm && Array.isArray(manifest.modes.wasm.lowerings)
|
|
967
|
+
? manifest.modes.wasm.lowerings.map(clone)
|
|
968
|
+
: [];
|
|
969
|
+
const canonicalEffects = entries.map((entry) => entry.canonicalEffect);
|
|
970
|
+
const canonicalIntrinsics = entries.map((entry) => entry.canonicalIntrinsic).filter(Boolean);
|
|
971
|
+
const artifact = normalizeArtifact({
|
|
972
|
+
version: gripContracts.GRIP_LOWERING_PLAN_VERSION,
|
|
973
|
+
generatedBy: inputs.generatedBy || PACKAGE_VERSION,
|
|
974
|
+
phase: 54,
|
|
975
|
+
artifact: gripContracts.GRIP_LOWERING_PLAN_ARTIFACT,
|
|
976
|
+
contractId: gripContracts.GRIP_CONTRACT_ID,
|
|
977
|
+
package: manifest.npmPackage,
|
|
978
|
+
lowerableSubpath: manifest.lowerableSubpath,
|
|
979
|
+
status: diagnosticHasError(diagnostics) ? 'error' : 'ok',
|
|
980
|
+
source: stableFileName(sourceFile.fileName || '<grip-lowering-plan>', inputs.cwd || process.cwd()),
|
|
981
|
+
facade: clone(manifest.facade || {}),
|
|
982
|
+
manifest: {
|
|
983
|
+
version: manifest.version,
|
|
984
|
+
owner: 'package',
|
|
985
|
+
package: manifest.npmPackage,
|
|
986
|
+
lowerableSubpath: manifest.lowerableSubpath
|
|
987
|
+
},
|
|
988
|
+
packageCompilerBuilder,
|
|
989
|
+
builderOwner: packageCompilerBuilder.owner,
|
|
990
|
+
lowerings,
|
|
991
|
+
entries,
|
|
992
|
+
canonicalEffects: canonicalEffects.filter(Boolean),
|
|
993
|
+
canonicalIntrinsics,
|
|
994
|
+
diagnostics,
|
|
995
|
+
policy: {
|
|
996
|
+
packageOwnsCompilerBuilder: true,
|
|
997
|
+
libraryKitOwnsGenericDiscovery: true,
|
|
998
|
+
compilerOwnsPackageMapping: false,
|
|
999
|
+
providerRuntimeImplemented: true,
|
|
1000
|
+
providerBehaviorImplemented: true,
|
|
1001
|
+
productionGripRuntimeBehavior: true,
|
|
1002
|
+
arbitraryThirdPartyLowererExecution: false,
|
|
1003
|
+
betaProofOnly: false,
|
|
1004
|
+
dynamicChannelsSupported: false,
|
|
1005
|
+
dynamicPublishPayloadsSupported: false,
|
|
1006
|
+
canonicalRootLowering: true,
|
|
1007
|
+
statelessFraming: true
|
|
1008
|
+
},
|
|
1009
|
+
summary: {
|
|
1010
|
+
entries: entries.length,
|
|
1011
|
+
holdEntries: entries.filter((entry) => entry.kind === 'grip-hold').length,
|
|
1012
|
+
channelEntries: entries.filter((entry) => entry.kind === 'grip-channel').length,
|
|
1013
|
+
publishEntries: entries.filter((entry) => entry.kind === 'grip-publish').length,
|
|
1014
|
+
canonicalRootEntries: entries.filter((entry) => ['grip-is-websocket', 'grip-subscribe', 'grip-handoff', 'grip-broadcast'].includes(entry.kind)).length,
|
|
1015
|
+
intrinsicEntries: canonicalIntrinsics.length,
|
|
1016
|
+
broadcastEntries: entries.filter((entry) => entry.kind === 'grip-broadcast').length,
|
|
1017
|
+
holdCalls: entries.filter((entry) => entry.kind === 'grip-hold').length,
|
|
1018
|
+
channelCalls: entries.filter((entry) => entry.kind === 'grip-channel').length,
|
|
1019
|
+
publishCalls: entries.filter((entry) => entry.kind === 'grip-publish').length,
|
|
1020
|
+
imports: bindings.imports.length,
|
|
1021
|
+
packageOwnedBuilder: packageCompilerBuilder.owner === '@pulse-compute/grip',
|
|
1022
|
+
manifestOwnedByPackage: manifest.npmPackage === '@pulse-compute/grip',
|
|
1023
|
+
sidecarDeclared: Boolean(manifest.modes && manifest.modes.wasm && manifest.modes.wasm.sidecar),
|
|
1024
|
+
sidecarSymbolsDeclared: lowerings.length >= 3,
|
|
1025
|
+
providerRuntimeImplemented: true,
|
|
1026
|
+
compiledWasmRuntimeImplemented: true,
|
|
1027
|
+
dynamicChannelsSupported: false,
|
|
1028
|
+
dynamicPublishPayloadsSupported: false,
|
|
1029
|
+
productionCompletenessRequired: true,
|
|
1030
|
+
errors: diagnostics.filter((entry) => (entry.severity || 'error') === 'error').length,
|
|
1031
|
+
warnings: diagnostics.filter((entry) => entry.severity === 'warning').length
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
return {
|
|
1036
|
+
artifact,
|
|
1037
|
+
entries,
|
|
1038
|
+
canonicalEffects: canonicalEffects.filter(Boolean),
|
|
1039
|
+
canonicalIntrinsics,
|
|
1040
|
+
diagnostics,
|
|
1041
|
+
manifest,
|
|
1042
|
+
packageCompilerBuilder,
|
|
1043
|
+
hasErrors: artifact.summary.errors > 0
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const gripLoweringBuilder = Object.freeze({
|
|
1048
|
+
owner: '@pulse-compute/grip',
|
|
1049
|
+
contractId: gripContracts.GRIP_CONTRACT_ID,
|
|
1050
|
+
artifact: gripContracts.GRIP_LOWERING_PLAN_ARTIFACT,
|
|
1051
|
+
build: buildGripLoweringPlan
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
const GRIP_DIAGNOSTIC_CODES = Object.freeze({
|
|
1055
|
+
...gripContracts.GRIP_DIAGNOSTIC_CODES,
|
|
1056
|
+
NON_LITERAL_MESSAGE: gripContracts.GRIP_DIAGNOSTIC_CODES.NON_LITERAL_PUBLISH_MESSAGE
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
module.exports = {
|
|
1060
|
+
buildGripLoweringPlan,
|
|
1061
|
+
gripLoweringBuilder,
|
|
1062
|
+
GRIP_DIAGNOSTIC_CODES: gripContracts.GRIP_DIAGNOSTIC_CODES
|
|
1063
|
+
};
|