@stackstackstack/dsh-api-gateway 0.1.5
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +42 -0
- package/README.zh.md +42 -0
- package/lib/client.js +420 -0
- package/lib/index.js +396 -0
- package/lib/invariant.js +24 -0
- package/lib/types/client/index.d.ts +23 -0
- package/lib/types/client/index.js +467 -0
- package/lib/types/index.d.ts +61 -0
- package/lib/types/index.js +508 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +23 -0
- package/lib/types/types.d.ts +34 -0
- package/lib/types/types.js +6 -0
- package/package.json +75 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client projection of generated Typert Remote descriptors. Contributions
|
|
3
|
+
* install traced `remote.<namespace>` services; no JavaScript Proxy
|
|
4
|
+
* participates in method lookup, invocation, or type exposure.
|
|
5
|
+
*/
|
|
6
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
7
|
+
/** Required Client services: the Typert registry and the existing Connection carrier. */
|
|
8
|
+
export const inject = ['typert', 'connection'];
|
|
9
|
+
/**
|
|
10
|
+
* Install the typed Client Remote service.
|
|
11
|
+
* @param ctx - Client Cordis root.
|
|
12
|
+
*/
|
|
13
|
+
export function apply(ctx) {
|
|
14
|
+
new ClientRemoteService(ctx);
|
|
15
|
+
}
|
|
16
|
+
class ClientRemoteService extends Service {
|
|
17
|
+
ownerCtx;
|
|
18
|
+
namespaces = new Map();
|
|
19
|
+
subscriptions = new Map();
|
|
20
|
+
mutations = Promise.resolve();
|
|
21
|
+
constructor(ctx) {
|
|
22
|
+
super(ctx, 'remote');
|
|
23
|
+
this.ownerCtx = ctx;
|
|
24
|
+
ctx.effect(() => () => { this.subscriptions.clear(); }, 'api-gateway.client.subscriptions');
|
|
25
|
+
}
|
|
26
|
+
async $mount(contribution) {
|
|
27
|
+
const callerCtx = this.ctx;
|
|
28
|
+
const owned = callerCtx.effect(async () => {
|
|
29
|
+
const dispose = await this.enqueue(() => this.mountContribution(callerCtx, contribution));
|
|
30
|
+
return () => this.enqueue(dispose);
|
|
31
|
+
}, `api-gateway.client.$mount(${JSON.stringify(contribution.package)})`);
|
|
32
|
+
await owned;
|
|
33
|
+
return async () => { await owned(); };
|
|
34
|
+
}
|
|
35
|
+
$on(event, listener) {
|
|
36
|
+
// The table is keyed by the runtime event name, so the argument list this
|
|
37
|
+
// signature pins per event cannot survive in it; `$deliver` restores it
|
|
38
|
+
// from the frame the Host emitted for that same name.
|
|
39
|
+
const subscription = { listener };
|
|
40
|
+
const owned = this.ctx.effect(() => {
|
|
41
|
+
const listeners = this.listeners(event);
|
|
42
|
+
listeners.push(subscription);
|
|
43
|
+
return () => {
|
|
44
|
+
const at = listeners.indexOf(subscription);
|
|
45
|
+
/* v8 ignore next -- listener */
|
|
46
|
+
if (at >= 0)
|
|
47
|
+
listeners.splice(at, 1);
|
|
48
|
+
};
|
|
49
|
+
}, `api-gateway.client.$on(${JSON.stringify(event)})`);
|
|
50
|
+
return () => { void owned(); };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Deliver one forwarded event in registration order, isolating a listener
|
|
54
|
+
* that fails either synchronously or by rejecting a returned promise; see
|
|
55
|
+
* {@link TypertClientRemote.$dispatch} for the caller contract.
|
|
56
|
+
*/
|
|
57
|
+
$dispatch(event, args) {
|
|
58
|
+
const listeners = this.subscriptions.get(event);
|
|
59
|
+
if (listeners === undefined)
|
|
60
|
+
return;
|
|
61
|
+
// Snapshot: a listener may subscribe or dispose during delivery, and this
|
|
62
|
+
// round's recipients are the ones registered when the frame arrived.
|
|
63
|
+
for (const { listener } of [...listeners]) {
|
|
64
|
+
const report = (error) => {
|
|
65
|
+
console.error(`client api: Remote event ${JSON.stringify(event)} listener threw:`, error);
|
|
66
|
+
};
|
|
67
|
+
try {
|
|
68
|
+
/* oxlint-disable-next-line typescript/no-confusing-void-expression --
|
|
69
|
+
* The declared return is void, so nobody awaits an async listener; the
|
|
70
|
+
* runtime value is still a promise, and reading it is the only way to
|
|
71
|
+
* keep its rejection inside this containment instead of surfacing as an
|
|
72
|
+
* unhandled one. */
|
|
73
|
+
const settled = listener(...args);
|
|
74
|
+
if (settled instanceof Promise)
|
|
75
|
+
settled.catch(report);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
report(error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Subscriptions for one event name; empty arrays are retained, bounded by the Host's selection. */
|
|
83
|
+
listeners(event) {
|
|
84
|
+
let listeners = this.subscriptions.get(event);
|
|
85
|
+
if (listeners === undefined) {
|
|
86
|
+
listeners = [];
|
|
87
|
+
this.subscriptions.set(event, listeners);
|
|
88
|
+
}
|
|
89
|
+
return listeners;
|
|
90
|
+
}
|
|
91
|
+
enqueue(operation) {
|
|
92
|
+
const result = this.mutations.then(operation, operation);
|
|
93
|
+
this.mutations = result.then(() => undefined, () => undefined);
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
async mountContribution(callerCtx, contribution) {
|
|
97
|
+
this.validateContribution(contribution);
|
|
98
|
+
const disposeRemote = callerCtx.typert.remotes.register(contribution);
|
|
99
|
+
const installed = [];
|
|
100
|
+
try {
|
|
101
|
+
for (const descriptor of contribution.descriptors)
|
|
102
|
+
installed.push(await this.install(descriptor));
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
for (const dispose of installed.reverse())
|
|
106
|
+
await dispose();
|
|
107
|
+
await disposeRemote();
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
return async () => {
|
|
111
|
+
for (const dispose of installed.reverse())
|
|
112
|
+
await dispose();
|
|
113
|
+
await disposeRemote();
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
validateContribution(contribution) {
|
|
117
|
+
const direct = new Map();
|
|
118
|
+
const scoped = new Map();
|
|
119
|
+
const add = (table, descriptor, kind) => {
|
|
120
|
+
const methods = table.get(descriptor.namespace) ?? new Set();
|
|
121
|
+
if (methods.has(descriptor.method)) {
|
|
122
|
+
throw new Error(`client api: contribution repeats ${kind} method ${endpointOf(descriptor)}`);
|
|
123
|
+
}
|
|
124
|
+
methods.add(descriptor.method);
|
|
125
|
+
table.set(descriptor.namespace, methods);
|
|
126
|
+
const namespace = this.namespaces.get(descriptor.namespace)?.service;
|
|
127
|
+
if (namespace?.has(kind, descriptor.method) === true) {
|
|
128
|
+
throw new Error(`client api: ${kind} method ${endpointOf(descriptor)} is already mounted`);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
for (const descriptor of contribution.descriptors) {
|
|
132
|
+
requireStrictDescriptor(descriptor);
|
|
133
|
+
if (descriptor.invocation.kind === 'direct')
|
|
134
|
+
add(direct, descriptor, 'direct');
|
|
135
|
+
if (scopedProjection(descriptor) !== undefined)
|
|
136
|
+
add(scoped, descriptor, 'scoped');
|
|
137
|
+
}
|
|
138
|
+
const namespaces = new Set([...direct.keys(), ...scoped.keys()]);
|
|
139
|
+
for (const namespace of namespaces) {
|
|
140
|
+
const service = this.namespaces.get(namespace)?.service;
|
|
141
|
+
if (service === undefined) {
|
|
142
|
+
if (namespace in this) {
|
|
143
|
+
throw new Error(`client api: namespace ${JSON.stringify(namespace)} conflicts with the Remote service`);
|
|
144
|
+
}
|
|
145
|
+
const serviceKey = remoteServiceKey(namespace);
|
|
146
|
+
const property = this.ownerCtx.reflect.props[serviceKey];
|
|
147
|
+
if (property?.type === 'accessor' || this.ownerCtx.get(serviceKey) !== undefined) {
|
|
148
|
+
throw new Error(`client api: namespace ${JSON.stringify(namespace)} conflicts with an existing Remote namespace`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const method of new Set([...(direct.get(namespace) ?? []), ...(scoped.get(namespace) ?? [])])) {
|
|
152
|
+
if (service === undefined)
|
|
153
|
+
RemoteNamespaceService.assertMethodAvailable(namespace, method);
|
|
154
|
+
else
|
|
155
|
+
service.assertMethodAvailable(method);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async install(descriptor) {
|
|
160
|
+
const token = { active: true, abort: new AbortController() };
|
|
161
|
+
const installed = [];
|
|
162
|
+
try {
|
|
163
|
+
if (descriptor.invocation.kind === 'direct') {
|
|
164
|
+
installed.push(await this.installDirect(descriptor, token));
|
|
165
|
+
}
|
|
166
|
+
const projection = scopedProjection(descriptor);
|
|
167
|
+
if (projection !== undefined)
|
|
168
|
+
installed.push(await this.installScoped(descriptor, projection, token));
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
token.active = false;
|
|
172
|
+
token.abort.abort();
|
|
173
|
+
for (const dispose of installed.reverse())
|
|
174
|
+
await dispose();
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
return async () => {
|
|
178
|
+
/* v8 ignore next -- Cordis effect disposers are idempotent and invoke this cleanup at most once. */
|
|
179
|
+
if (!token.active)
|
|
180
|
+
return;
|
|
181
|
+
token.active = false;
|
|
182
|
+
token.abort.abort();
|
|
183
|
+
for (const dispose of installed.reverse())
|
|
184
|
+
await dispose();
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
async installDirect(descriptor, token) {
|
|
188
|
+
const namespace = await this.namespace(descriptor.namespace);
|
|
189
|
+
try {
|
|
190
|
+
namespace.service.installDirect(descriptor, token);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
await this.disposeNamespace(descriptor.namespace, namespace);
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
return async () => {
|
|
197
|
+
namespace.service.remove('direct', descriptor.method, token);
|
|
198
|
+
await this.disposeNamespace(descriptor.namespace, namespace);
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async installScoped(descriptor, projection, token) {
|
|
202
|
+
const namespace = await this.namespace(descriptor.namespace);
|
|
203
|
+
try {
|
|
204
|
+
namespace.service.installScoped(descriptor, projection, token);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
await this.disposeNamespace(descriptor.namespace, namespace);
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
return async () => {
|
|
211
|
+
namespace.service.remove('scoped', descriptor.method, token);
|
|
212
|
+
await this.disposeNamespace(descriptor.namespace, namespace);
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
async namespace(name) {
|
|
216
|
+
let namespace = this.namespaces.get(name);
|
|
217
|
+
if (namespace !== undefined)
|
|
218
|
+
return namespace;
|
|
219
|
+
let service;
|
|
220
|
+
const fiber = this.ownerCtx.plugin({
|
|
221
|
+
name: remoteServiceKey(name),
|
|
222
|
+
apply: (ctx) => {
|
|
223
|
+
service = new RemoteNamespaceService(ctx, name, (direct, scoped, caller, args) => this.invokeMethod(direct, scoped, caller, args));
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
try {
|
|
227
|
+
await fiber;
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
await fiber.dispose();
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
/* v8 ignore next -- a settled namespace fiber synchronously constructs its Service. */
|
|
234
|
+
if (service === undefined)
|
|
235
|
+
throw new Error(`client api: namespace ${JSON.stringify(name)} did not start`);
|
|
236
|
+
namespace = { service, dispose: fiber.dispose };
|
|
237
|
+
this.namespaces.set(name, namespace);
|
|
238
|
+
return namespace;
|
|
239
|
+
}
|
|
240
|
+
async disposeNamespace(name, namespace) {
|
|
241
|
+
if (!namespace.service.empty || this.namespaces.get(name) !== namespace)
|
|
242
|
+
return;
|
|
243
|
+
this.namespaces.delete(name);
|
|
244
|
+
await namespace.dispose();
|
|
245
|
+
}
|
|
246
|
+
invokeMethod(direct, scoped, callerCtx, values) {
|
|
247
|
+
if (scoped !== undefined) {
|
|
248
|
+
const binder = this.ownerCtx.typert.contexts.getClient(scoped.projection.context);
|
|
249
|
+
const identity = binder?.identity(callerCtx);
|
|
250
|
+
if (identity !== undefined) {
|
|
251
|
+
return this.invoke(scoped.descriptor, scoped.projection, scoped.token, callerCtx, values, { value: identity });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (direct !== undefined) {
|
|
255
|
+
return this.invoke(direct.descriptor, undefined, direct.token, callerCtx, values);
|
|
256
|
+
}
|
|
257
|
+
if (scoped !== undefined) {
|
|
258
|
+
return this.invoke(scoped.descriptor, scoped.projection, scoped.token, callerCtx, values);
|
|
259
|
+
}
|
|
260
|
+
throw new Error('client api: Remote method is no longer mounted');
|
|
261
|
+
}
|
|
262
|
+
async invoke(descriptor, projection, token, callerCtx, values, boundIdentity) {
|
|
263
|
+
const endpoint = endpointOf(descriptor);
|
|
264
|
+
if (!token.active)
|
|
265
|
+
return withdrawn(endpoint);
|
|
266
|
+
const expected = descriptor.parameters.length - (projection?.parameterIndex === undefined ? 0 : 1);
|
|
267
|
+
const hasCallerSignal = descriptor.cancellation !== undefined && values.length === expected + 1;
|
|
268
|
+
if (values.length !== expected && !hasCallerSignal) {
|
|
269
|
+
const contract = descriptor.cancellation === undefined
|
|
270
|
+
? `${String(expected)} argument(s)`
|
|
271
|
+
: `${String(expected)} business argument(s) plus an optional AbortSignal`;
|
|
272
|
+
throw new Error(`client api: ${endpoint} expected ${contract}, got ${String(values.length)}`);
|
|
273
|
+
}
|
|
274
|
+
const args = Object.create(null);
|
|
275
|
+
if (projection !== undefined) {
|
|
276
|
+
const binder = boundIdentity === undefined
|
|
277
|
+
? this.ownerCtx.typert.contexts.getClient(projection.context)
|
|
278
|
+
: undefined;
|
|
279
|
+
if (boundIdentity === undefined && binder === undefined) {
|
|
280
|
+
throw new Error(`client api: ${endpoint} has no Client Context binder for ${JSON.stringify(projection.context)}`);
|
|
281
|
+
}
|
|
282
|
+
const identity = boundIdentity === undefined
|
|
283
|
+
? binder?.identity(callerCtx)
|
|
284
|
+
: boundIdentity.value;
|
|
285
|
+
if (identity === undefined) {
|
|
286
|
+
throw new Error(`client api: ${endpoint} requires a ${JSON.stringify(projection.context)} Context`);
|
|
287
|
+
}
|
|
288
|
+
args[projection.wire] = parse(projection.codec, identity, endpoint, projection.wire);
|
|
289
|
+
}
|
|
290
|
+
let valueIndex = 0;
|
|
291
|
+
descriptor.parameters.forEach((parameter, parameterIndex) => {
|
|
292
|
+
if (parameterIndex === projection?.parameterIndex)
|
|
293
|
+
return;
|
|
294
|
+
const value = parse(parameter.codec, values[valueIndex], endpoint, parameter.wire);
|
|
295
|
+
if (value !== undefined)
|
|
296
|
+
args[parameter.wire] = value;
|
|
297
|
+
valueIndex += 1;
|
|
298
|
+
});
|
|
299
|
+
const connection = this.ownerCtx.get('connection');
|
|
300
|
+
if (connection === undefined)
|
|
301
|
+
throw new Error(`client api: ${endpoint} has no active Connection`);
|
|
302
|
+
const callerSignal = hasCallerSignal ? values[expected] : undefined;
|
|
303
|
+
const signal = callerSignal === undefined
|
|
304
|
+
? token.abort.signal
|
|
305
|
+
: AbortSignal.any([token.abort.signal, callerSignal]);
|
|
306
|
+
try {
|
|
307
|
+
const result = await connection.rpc.call('/api', endpoint, { args }, signal);
|
|
308
|
+
if (!mountActive(token))
|
|
309
|
+
return withdrawn(endpoint);
|
|
310
|
+
if (!result.ok)
|
|
311
|
+
return { ok: false, error: result.error };
|
|
312
|
+
return { ok: true, value: parse(descriptor.result, result.value, endpoint, 'result') };
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
// Carrier throws (offline, abort, a rejected result payload) are outcomes
|
|
316
|
+
// of the call, not assembly faults, so they join the same error branch.
|
|
317
|
+
return carrierFailure(endpoint, error);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
class RemoteNamespaceService extends Service {
|
|
322
|
+
invokeRemote;
|
|
323
|
+
methods = new Map();
|
|
324
|
+
namespace;
|
|
325
|
+
static assertMethodAvailable(namespace, method) {
|
|
326
|
+
if (REMOTE_NAMESPACE_FIELDS.has(method) || method in RemoteNamespaceService.prototype) {
|
|
327
|
+
throw new Error(`client api: method ${JSON.stringify(`${namespace}/${method}`)} conflicts with its namespace service`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
constructor(ctx, name, invokeRemote) {
|
|
331
|
+
super(ctx, remoteServiceKey(name));
|
|
332
|
+
this.invokeRemote = invokeRemote;
|
|
333
|
+
this.namespace = name;
|
|
334
|
+
}
|
|
335
|
+
assertMethodAvailable(method) {
|
|
336
|
+
RemoteNamespaceService.assertMethodAvailable(this.namespace, method);
|
|
337
|
+
if (method in this && !this.methods.has(method)) {
|
|
338
|
+
throw new Error(`client api: method ${JSON.stringify(`${this.namespace}/${method}`)} conflicts with its namespace service`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
get empty() {
|
|
342
|
+
return this.methods.size === 0;
|
|
343
|
+
}
|
|
344
|
+
has(kind, method) {
|
|
345
|
+
return this.methods.get(method)?.[kind] !== undefined;
|
|
346
|
+
}
|
|
347
|
+
installDirect(descriptor, token) {
|
|
348
|
+
this.install(descriptor.method, 'direct', { descriptor, token });
|
|
349
|
+
}
|
|
350
|
+
installScoped(descriptor, projection, token) {
|
|
351
|
+
this.install(descriptor.method, 'scoped', { descriptor, projection, token });
|
|
352
|
+
}
|
|
353
|
+
install(method, kind, value) {
|
|
354
|
+
this.assertMethodAvailable(method);
|
|
355
|
+
let record = this.methods.get(method);
|
|
356
|
+
const fresh = record === undefined;
|
|
357
|
+
record ??= {};
|
|
358
|
+
if (fresh) {
|
|
359
|
+
Object.defineProperty(this, method, {
|
|
360
|
+
configurable: true,
|
|
361
|
+
enumerable: true,
|
|
362
|
+
get: function () {
|
|
363
|
+
const callerCtx = this.ctx;
|
|
364
|
+
const current = this.methods.get(method);
|
|
365
|
+
const direct = current?.direct;
|
|
366
|
+
const scoped = current?.scoped;
|
|
367
|
+
return (...args) => {
|
|
368
|
+
return this.invokeRemote(direct, scoped, callerCtx, args);
|
|
369
|
+
};
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
this.methods.set(method, record);
|
|
373
|
+
}
|
|
374
|
+
if (kind === 'direct')
|
|
375
|
+
record.direct = value;
|
|
376
|
+
else
|
|
377
|
+
record.scoped = value;
|
|
378
|
+
}
|
|
379
|
+
remove(kind, method, token) {
|
|
380
|
+
const record = this.methods.get(method);
|
|
381
|
+
const current = record?.[kind];
|
|
382
|
+
/* v8 ignore next -- duplicate live variants are rejected before installation, so no newer token can replace this one. */
|
|
383
|
+
if (record === undefined || current?.token !== token)
|
|
384
|
+
return;
|
|
385
|
+
if (kind === 'direct')
|
|
386
|
+
delete record.direct;
|
|
387
|
+
else
|
|
388
|
+
delete record.scoped;
|
|
389
|
+
if (record.direct !== undefined || record.scoped !== undefined)
|
|
390
|
+
return;
|
|
391
|
+
this.methods.delete(method);
|
|
392
|
+
Reflect.deleteProperty(this, method);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const REMOTE_NAMESPACE_FIELDS = new Set(['ctx', 'empty', 'invokeRemote', 'methods', 'name', 'namespace']);
|
|
396
|
+
function remoteServiceKey(namespace) {
|
|
397
|
+
return `remote.${namespace}`;
|
|
398
|
+
}
|
|
399
|
+
function endpointOf(descriptor) {
|
|
400
|
+
return `${descriptor.namespace}/${descriptor.method}`;
|
|
401
|
+
}
|
|
402
|
+
function mountActive(token) {
|
|
403
|
+
return token.active;
|
|
404
|
+
}
|
|
405
|
+
function scopedProjection(descriptor) {
|
|
406
|
+
if (descriptor.invocation.kind === 'context') {
|
|
407
|
+
return {
|
|
408
|
+
context: descriptor.invocation.context,
|
|
409
|
+
wire: descriptor.invocation.wire,
|
|
410
|
+
codec: descriptor.invocation.codec,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
if (descriptor.scope === undefined)
|
|
414
|
+
return undefined;
|
|
415
|
+
const lookupParameters = descriptor.parameters
|
|
416
|
+
.map((parameter, index) => ({ parameter, index }))
|
|
417
|
+
.filter(candidate => candidate.parameter.source === 'lookup');
|
|
418
|
+
const selected = lookupParameters.length === 1 ? lookupParameters[0] : undefined;
|
|
419
|
+
if (selected === undefined
|
|
420
|
+
|| selected.parameter.wire !== descriptor.scope.wire
|
|
421
|
+
|| selected.parameter.lookup !== descriptor.scope.context) {
|
|
422
|
+
throw new Error(`client api: generated Remote ${endpointOf(descriptor)} scope must select its only lookup parameter`);
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
context: descriptor.scope.context,
|
|
426
|
+
wire: descriptor.scope.wire,
|
|
427
|
+
codec: selected.parameter.codec,
|
|
428
|
+
parameterIndex: selected.index,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function requireStrictDescriptor(descriptor) {
|
|
432
|
+
const endpoint = endpointOf(descriptor);
|
|
433
|
+
requireStrictCodec(descriptor.result, endpoint, 'result');
|
|
434
|
+
for (const parameter of descriptor.parameters) {
|
|
435
|
+
requireStrictCodec(parameter.codec, endpoint, parameter.wire);
|
|
436
|
+
}
|
|
437
|
+
if (descriptor.invocation.kind === 'context') {
|
|
438
|
+
requireStrictCodec(descriptor.invocation.codec, endpoint, descriptor.invocation.wire);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function requireStrictCodec(codec, endpoint, field) {
|
|
442
|
+
if (codec.mode !== 'strict') {
|
|
443
|
+
throw new Error(`client api: generated Remote ${endpoint} field ${JSON.stringify(field)} has no strict codec`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function parse(codec, value, endpoint, field) {
|
|
447
|
+
if (codec.mode !== 'strict') {
|
|
448
|
+
throw new Error(`client api: generated Remote ${endpoint} field ${JSON.stringify(field)} has no strict codec`);
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
return codec.schema.parse(value);
|
|
452
|
+
}
|
|
453
|
+
catch (cause) {
|
|
454
|
+
throw new Error(`client api: ${endpoint} rejected ${JSON.stringify(field)}`, { cause });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/** The namespace retired before or during the call, so no request outcome exists. */
|
|
458
|
+
function withdrawn(endpoint) {
|
|
459
|
+
return internalFailure(`client api: Remote method ${endpoint} is no longer mounted`);
|
|
460
|
+
}
|
|
461
|
+
function carrierFailure(endpoint, error) {
|
|
462
|
+
return internalFailure(`client api: ${endpoint} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
463
|
+
}
|
|
464
|
+
function internalFailure(message) {
|
|
465
|
+
return { ok: false, error: { code: 'internal', message, details: {} } };
|
|
466
|
+
}
|
|
467
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Typert Remote dispatch over Cordis Services and registered providers.
|
|
3
|
+
* Transport, request correlation, and response envelopes belong to Connection.
|
|
4
|
+
* @module @stackstackstack/dsh-api-gateway
|
|
5
|
+
*/
|
|
6
|
+
import { Context, Service } from '@deepseek-ai/cordis';
|
|
7
|
+
import type { InvokeRemoteRequest, TypertGateway, TypertGatewayErrorCode } from './types.ts';
|
|
8
|
+
export type { InvokeRemoteRequest, TypertGateway, TypertGatewayErrorCode, } from './types.ts';
|
|
9
|
+
interface GatewayErrorOptions {
|
|
10
|
+
readonly cause?: unknown;
|
|
11
|
+
readonly field?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Dispatch failure produced outside the invoked business method. */
|
|
14
|
+
export declare class TypertGatewayError extends Error {
|
|
15
|
+
/** Machine-readable failure category. */
|
|
16
|
+
readonly code: TypertGatewayErrorCode;
|
|
17
|
+
/** Canonical `<namespace>/<method>` endpoint. */
|
|
18
|
+
readonly endpoint: string;
|
|
19
|
+
/** Affected wire field when the failure is field-specific. */
|
|
20
|
+
readonly field: string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Construct a Gateway failure without embedding boundary values in its message.
|
|
23
|
+
* @param code - stable failure category.
|
|
24
|
+
* @param endpoint - canonical Remote endpoint.
|
|
25
|
+
* @param message - correction-oriented diagnostic without sensitive values.
|
|
26
|
+
* @param options - optional field and contained cause.
|
|
27
|
+
*/
|
|
28
|
+
constructor(code: TypertGatewayErrorCode, endpoint: string, message: string, options?: GatewayErrorOptions);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve strict generated definitions or conservative SRC markers against
|
|
32
|
+
* current Cordis Services and Typert providers.
|
|
33
|
+
* @typert service typertGateway
|
|
34
|
+
*/
|
|
35
|
+
export declare class TypertGatewayService extends Service implements TypertGateway {
|
|
36
|
+
static inject: string[];
|
|
37
|
+
private srcClaims;
|
|
38
|
+
/**
|
|
39
|
+
* Register the Gateway against the active Typert registry.
|
|
40
|
+
* @param ctx - owning Host Context with Typert registry access.
|
|
41
|
+
*/
|
|
42
|
+
constructor(ctx: Context);
|
|
43
|
+
private claimsEndpoint;
|
|
44
|
+
private collectSrcClaims;
|
|
45
|
+
/**
|
|
46
|
+
* Invoke one live Remote method through strict generated reflection or SRC markers.
|
|
47
|
+
* @param request - decoded endpoint and exact named wire arguments.
|
|
48
|
+
* @returns the validated business result.
|
|
49
|
+
* @throws {@link TypertGatewayError} for dispatch, provider, or boundary failures; lookup-policy and business errors retain identity.
|
|
50
|
+
*/
|
|
51
|
+
invoke(request: InvokeRemoteRequest): Promise<unknown>;
|
|
52
|
+
private dispatchRpc;
|
|
53
|
+
private invokeRpc;
|
|
54
|
+
private resolveDescriptor;
|
|
55
|
+
private resolveSrcDescriptor;
|
|
56
|
+
private srcDescriptor;
|
|
57
|
+
private resolveReceiverContext;
|
|
58
|
+
private resolveParameter;
|
|
59
|
+
}
|
|
60
|
+
export default TypertGatewayService;
|
|
61
|
+
//# sourceMappingURL=index.d.ts.map
|