depa-actor 0.2.1 → 0.2.2
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/dist/addressing.d.ts.map +1 -1
- package/dist/addressing.js +274 -166
- package/dist/addressing.js.map +1 -1
- package/dist-cjs/addressing.cjs +274 -166
- package/package.json +1 -1
- package/src/addressing.ts +415 -195
package/src/addressing.ts
CHANGED
|
@@ -125,8 +125,6 @@ export type TargetedActorProcessor<TRuntime, TSelector, TInvocation, TConfig, TO
|
|
|
125
125
|
config: TConfig,
|
|
126
126
|
) => TOutput | Promise<TOutput>;
|
|
127
127
|
|
|
128
|
-
type UnknownRecord = Record<string, unknown>;
|
|
129
|
-
|
|
130
128
|
const ADDRESS_KEYS = [
|
|
131
129
|
'schemaVersion',
|
|
132
130
|
'namespace',
|
|
@@ -151,6 +149,9 @@ const REGISTRATION_RECEIPT_KEYS = [
|
|
|
151
149
|
] as const;
|
|
152
150
|
const REGISTER_INPUT_KEYS = ['address', 'ownerSnapshot', 'endpoint'] as const;
|
|
153
151
|
const ENDPOINT_KEYS = ['handlerIdentity', 'dispatch'] as const;
|
|
152
|
+
const RUNTIME_CONFIG_KEYS = ['runtimeInstanceId', 'resolveAlias', 'registrationIdFactory'] as const;
|
|
153
|
+
const EXPECTED_REGISTRATION_ID_KEYS = ['expectedRegistrationId'] as const;
|
|
154
|
+
const DISPATCH_INVOCATION_REQUIRED_KEYS = ['input'] as const;
|
|
154
155
|
const ADDRESS_SEGMENT = /^[A-Za-z0-9](?:[A-Za-z0-9._~-]{0,126}[A-Za-z0-9])?$/;
|
|
155
156
|
const OPAQUE_TOKEN = /^[A-Za-z0-9](?:[A-Za-z0-9._~:/-]{0,254}[A-Za-z0-9])?$/;
|
|
156
157
|
|
|
@@ -161,38 +162,126 @@ interface LocalRegistration<TInvocation, TOutput> {
|
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
interface LocalActorAddressingState<TInvocation, TOutput> {
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
registrations: Map<string, LocalRegistration<TInvocation, TOutput>>;
|
|
166
|
+
usedRegistrationIds: Set<string>;
|
|
167
|
+
readonly runtimeNonce: string;
|
|
166
168
|
readonly resolveAlias?: (alias: string) => ActorAddress | undefined;
|
|
167
169
|
readonly registrationIdFactory: NonNullable<
|
|
168
170
|
LocalActorAddressingRuntimeConfig['registrationIdFactory']
|
|
169
171
|
>;
|
|
170
172
|
sequence: number;
|
|
173
|
+
mutationVersion: number;
|
|
174
|
+
mutationGuard: string | undefined;
|
|
171
175
|
}
|
|
172
176
|
|
|
173
177
|
const runtimeStates = new WeakMap<object, LocalActorAddressingState<unknown, unknown>>();
|
|
178
|
+
let runtimeNonceSequence = 0;
|
|
179
|
+
|
|
180
|
+
function closedRecordError(
|
|
181
|
+
code: ActorAddressingErrorCode,
|
|
182
|
+
label: string,
|
|
183
|
+
detail: string,
|
|
184
|
+
): ActorAddressingError {
|
|
185
|
+
return new ActorAddressingError(code, `${label} ${detail}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function readClosedRecord(
|
|
189
|
+
value: unknown,
|
|
190
|
+
requiredKeys: readonly string[],
|
|
191
|
+
optionalKeys: readonly string[],
|
|
192
|
+
code: ActorAddressingErrorCode,
|
|
193
|
+
label: string,
|
|
194
|
+
): ReadonlyMap<string, unknown> {
|
|
195
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
196
|
+
throw closedRecordError(code, label, 'must be an object');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const prototype = Reflect.getPrototypeOf(value);
|
|
200
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
201
|
+
throw closedRecordError(code, label, 'must use Object.prototype or a null prototype');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const allowedKeys = new Set([...requiredKeys, ...optionalKeys]);
|
|
205
|
+
const values = new Map<string, unknown>();
|
|
206
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
207
|
+
if (typeof key !== 'string' || !allowedKeys.has(key)) {
|
|
208
|
+
throw closedRecordError(
|
|
209
|
+
code,
|
|
210
|
+
label,
|
|
211
|
+
`must contain only: ${[...allowedKeys].sort().join(', ')}`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
|
|
215
|
+
if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
|
|
216
|
+
throw closedRecordError(code, label, `${key} must be an own enumerable data property`);
|
|
217
|
+
}
|
|
218
|
+
values.set(key, descriptor.value);
|
|
219
|
+
}
|
|
174
220
|
|
|
175
|
-
|
|
176
|
-
|
|
221
|
+
for (const key of requiredKeys) {
|
|
222
|
+
if (!values.has(key)) {
|
|
223
|
+
throw closedRecordError(code, label, `must contain required field: ${key}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return values;
|
|
177
227
|
}
|
|
178
228
|
|
|
179
|
-
function
|
|
229
|
+
function readExactRecord(
|
|
180
230
|
value: unknown,
|
|
181
231
|
keys: readonly string[],
|
|
182
232
|
code: ActorAddressingErrorCode,
|
|
183
233
|
label: string,
|
|
184
|
-
):
|
|
185
|
-
|
|
186
|
-
|
|
234
|
+
): ReadonlyMap<string, unknown> {
|
|
235
|
+
return readClosedRecord(value, keys, [], code, label);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function readClosedArray(
|
|
239
|
+
value: unknown,
|
|
240
|
+
code: ActorAddressingErrorCode,
|
|
241
|
+
label: string,
|
|
242
|
+
): readonly unknown[] {
|
|
243
|
+
if (!Array.isArray(value) || Reflect.getPrototypeOf(value) !== Array.prototype) {
|
|
244
|
+
throw closedRecordError(code, label, 'must be a plain array');
|
|
187
245
|
}
|
|
188
|
-
|
|
189
|
-
const
|
|
246
|
+
|
|
247
|
+
const lengthDescriptor = Reflect.getOwnPropertyDescriptor(value, 'length');
|
|
248
|
+
const lengthValue =
|
|
249
|
+
lengthDescriptor && 'value' in lengthDescriptor ? lengthDescriptor.value : undefined;
|
|
190
250
|
if (
|
|
191
|
-
|
|
192
|
-
|
|
251
|
+
!lengthDescriptor ||
|
|
252
|
+
!('value' in lengthDescriptor) ||
|
|
253
|
+
lengthDescriptor.enumerable ||
|
|
254
|
+
typeof lengthValue !== 'number' ||
|
|
255
|
+
!Number.isSafeInteger(lengthValue) ||
|
|
256
|
+
lengthValue < 0
|
|
193
257
|
) {
|
|
194
|
-
throw
|
|
258
|
+
throw closedRecordError(code, label, 'must have the standard own data length property');
|
|
195
259
|
}
|
|
260
|
+
|
|
261
|
+
const length = lengthValue;
|
|
262
|
+
const expectedKeys = new Set<string>(['length']);
|
|
263
|
+
for (let index = 0; index < length; index += 1) {
|
|
264
|
+
expectedKeys.add(String(index));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
268
|
+
if (
|
|
269
|
+
ownKeys.length !== expectedKeys.size ||
|
|
270
|
+
ownKeys.some((key) => typeof key !== 'string' || !expectedKeys.has(key))
|
|
271
|
+
) {
|
|
272
|
+
throw closedRecordError(code, label, 'must be dense and contain no extra own keys');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const elements: unknown[] = [];
|
|
276
|
+
for (let index = 0; index < length; index += 1) {
|
|
277
|
+
const key = String(index);
|
|
278
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
|
|
279
|
+
if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
|
|
280
|
+
throw closedRecordError(code, label, `${key} must be an own enumerable data property`);
|
|
281
|
+
}
|
|
282
|
+
elements.push(descriptor.value);
|
|
283
|
+
}
|
|
284
|
+
return elements;
|
|
196
285
|
}
|
|
197
286
|
|
|
198
287
|
function parseAddressSegment(
|
|
@@ -229,16 +318,16 @@ function parseOpaqueToken(
|
|
|
229
318
|
}
|
|
230
319
|
|
|
231
320
|
export function parseActorAddress(value: unknown): ActorAddress {
|
|
232
|
-
|
|
233
|
-
if (
|
|
321
|
+
const record = readExactRecord(value, ADDRESS_KEYS, 'invalid-address', 'ActorAddress');
|
|
322
|
+
if (record.get('schemaVersion') !== ACTOR_ADDRESS_SCHEMA_VERSION) {
|
|
234
323
|
throw new ActorAddressingError('invalid-address', 'Unsupported ActorAddress schemaVersion');
|
|
235
324
|
}
|
|
236
325
|
return Object.freeze({
|
|
237
326
|
schemaVersion: ACTOR_ADDRESS_SCHEMA_VERSION,
|
|
238
|
-
namespace: parseAddressSegment(
|
|
239
|
-
deploymentId: parseAddressSegment(
|
|
240
|
-
actorKind: parseAddressSegment(
|
|
241
|
-
logicalKey: parseAddressSegment(
|
|
327
|
+
namespace: parseAddressSegment(record.get('namespace'), 'namespace'),
|
|
328
|
+
deploymentId: parseAddressSegment(record.get('deploymentId'), 'deploymentId'),
|
|
329
|
+
actorKind: parseAddressSegment(record.get('actorKind'), 'actorKind'),
|
|
330
|
+
logicalKey: parseAddressSegment(record.get('logicalKey'), 'logicalKey'),
|
|
242
331
|
});
|
|
243
332
|
}
|
|
244
333
|
|
|
@@ -254,19 +343,22 @@ export function actorAddressKey(address: ActorAddress): string {
|
|
|
254
343
|
}
|
|
255
344
|
|
|
256
345
|
export function parseActorSelector(value: unknown): ActorSelector {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
346
|
+
const record = readClosedRecord(
|
|
347
|
+
value,
|
|
348
|
+
[],
|
|
349
|
+
['byAddress', 'byAlias'],
|
|
350
|
+
'invalid-selector',
|
|
351
|
+
'ActorSelector',
|
|
352
|
+
);
|
|
353
|
+
if (record.size !== 1) {
|
|
262
354
|
throw new ActorAddressingError(
|
|
263
355
|
'invalid-selector',
|
|
264
356
|
'ActorSelector must contain exactly one of byAddress or byAlias',
|
|
265
357
|
);
|
|
266
358
|
}
|
|
267
|
-
if (
|
|
359
|
+
if (record.has('byAddress')) {
|
|
268
360
|
try {
|
|
269
|
-
return Object.freeze({ byAddress: parseActorAddress(
|
|
361
|
+
return Object.freeze({ byAddress: parseActorAddress(record.get('byAddress')) });
|
|
270
362
|
} catch (error) {
|
|
271
363
|
if (error instanceof ActorAddressingError) {
|
|
272
364
|
throw new ActorAddressingError('invalid-selector', `Invalid byAddress: ${error.message}`);
|
|
@@ -275,23 +367,30 @@ export function parseActorSelector(value: unknown): ActorSelector {
|
|
|
275
367
|
}
|
|
276
368
|
}
|
|
277
369
|
return Object.freeze({
|
|
278
|
-
byAlias: parseOpaqueToken(
|
|
370
|
+
byAlias: parseOpaqueToken(record.get('byAlias'), 'byAlias', 'invalid-selector'),
|
|
279
371
|
});
|
|
280
372
|
}
|
|
281
373
|
|
|
282
374
|
export function parseActorOwnerSnapshot(value: unknown): ActorOwnerSnapshot {
|
|
283
|
-
|
|
284
|
-
|
|
375
|
+
const record = readExactRecord(
|
|
376
|
+
value,
|
|
377
|
+
OWNER_SNAPSHOT_KEYS,
|
|
378
|
+
'invalid-owner-snapshot',
|
|
379
|
+
'ActorOwnerSnapshot',
|
|
380
|
+
);
|
|
381
|
+
if (record.get('schemaVersion') !== ACTOR_OWNER_SNAPSHOT_SCHEMA_VERSION) {
|
|
285
382
|
throw new ActorAddressingError('invalid-owner-snapshot', 'Unsupported owner snapshot schemaVersion');
|
|
286
383
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
384
|
+
const sourceRegistrations = readClosedArray(
|
|
385
|
+
record.get('registrations'),
|
|
386
|
+
'invalid-owner-snapshot',
|
|
387
|
+
'registrations',
|
|
388
|
+
);
|
|
290
389
|
|
|
291
390
|
const registrations: ActorOwnerSnapshotRegistration[] = [];
|
|
292
391
|
const seen = new Set<string>();
|
|
293
|
-
for (const registration of
|
|
294
|
-
|
|
392
|
+
for (const registration of sourceRegistrations) {
|
|
393
|
+
const registrationRecord = readExactRecord(
|
|
295
394
|
registration,
|
|
296
395
|
OWNER_REGISTRATION_KEYS,
|
|
297
396
|
'invalid-owner-snapshot',
|
|
@@ -299,7 +398,7 @@ export function parseActorOwnerSnapshot(value: unknown): ActorOwnerSnapshot {
|
|
|
299
398
|
);
|
|
300
399
|
let address: ActorAddress;
|
|
301
400
|
try {
|
|
302
|
-
address = parseActorAddress(
|
|
401
|
+
address = parseActorAddress(registrationRecord.get('address'));
|
|
303
402
|
} catch (error) {
|
|
304
403
|
if (error instanceof ActorAddressingError) {
|
|
305
404
|
throw new ActorAddressingError('invalid-owner-snapshot', error.message);
|
|
@@ -318,7 +417,7 @@ export function parseActorOwnerSnapshot(value: unknown): ActorOwnerSnapshot {
|
|
|
318
417
|
Object.freeze({
|
|
319
418
|
address,
|
|
320
419
|
handlerIdentity: parseOpaqueToken(
|
|
321
|
-
|
|
420
|
+
registrationRecord.get('handlerIdentity'),
|
|
322
421
|
'handlerIdentity',
|
|
323
422
|
'invalid-owner-snapshot',
|
|
324
423
|
),
|
|
@@ -329,12 +428,12 @@ export function parseActorOwnerSnapshot(value: unknown): ActorOwnerSnapshot {
|
|
|
329
428
|
return Object.freeze({
|
|
330
429
|
schemaVersion: ACTOR_OWNER_SNAPSHOT_SCHEMA_VERSION,
|
|
331
430
|
ownerRevision: parseOpaqueToken(
|
|
332
|
-
|
|
431
|
+
record.get('ownerRevision'),
|
|
333
432
|
'ownerRevision',
|
|
334
433
|
'invalid-owner-snapshot',
|
|
335
434
|
),
|
|
336
435
|
snapshotReceipt: parseOpaqueToken(
|
|
337
|
-
|
|
436
|
+
record.get('snapshotReceipt'),
|
|
338
437
|
'snapshotReceipt',
|
|
339
438
|
'invalid-owner-snapshot',
|
|
340
439
|
),
|
|
@@ -343,35 +442,35 @@ export function parseActorOwnerSnapshot(value: unknown): ActorOwnerSnapshot {
|
|
|
343
442
|
}
|
|
344
443
|
|
|
345
444
|
export function parseActorRegistrationReceipt(value: unknown): ActorRegistrationReceipt {
|
|
346
|
-
|
|
445
|
+
const record = readExactRecord(
|
|
347
446
|
value,
|
|
348
447
|
REGISTRATION_RECEIPT_KEYS,
|
|
349
448
|
'registration-mismatch',
|
|
350
449
|
'ActorRegistrationReceipt',
|
|
351
450
|
);
|
|
352
|
-
if (
|
|
451
|
+
if (record.get('schemaVersion') !== ACTOR_REGISTRATION_SCHEMA_VERSION) {
|
|
353
452
|
throw new ActorAddressingError('registration-mismatch', 'Unsupported registration schemaVersion');
|
|
354
453
|
}
|
|
355
454
|
return Object.freeze({
|
|
356
455
|
schemaVersion: ACTOR_REGISTRATION_SCHEMA_VERSION,
|
|
357
|
-
address: parseActorAddress(
|
|
456
|
+
address: parseActorAddress(record.get('address')),
|
|
358
457
|
ownerRevision: parseOpaqueToken(
|
|
359
|
-
|
|
458
|
+
record.get('ownerRevision'),
|
|
360
459
|
'ownerRevision',
|
|
361
460
|
'registration-mismatch',
|
|
362
461
|
),
|
|
363
462
|
snapshotReceipt: parseOpaqueToken(
|
|
364
|
-
|
|
463
|
+
record.get('snapshotReceipt'),
|
|
365
464
|
'snapshotReceipt',
|
|
366
465
|
'registration-mismatch',
|
|
367
466
|
),
|
|
368
467
|
handlerIdentity: parseOpaqueToken(
|
|
369
|
-
|
|
468
|
+
record.get('handlerIdentity'),
|
|
370
469
|
'handlerIdentity',
|
|
371
470
|
'registration-mismatch',
|
|
372
471
|
),
|
|
373
472
|
registrationId: parseOpaqueToken(
|
|
374
|
-
|
|
473
|
+
record.get('registrationId'),
|
|
375
474
|
'registrationId',
|
|
376
475
|
'registration-mismatch',
|
|
377
476
|
),
|
|
@@ -379,9 +478,7 @@ export function parseActorRegistrationReceipt(value: unknown): ActorRegistration
|
|
|
379
478
|
}
|
|
380
479
|
|
|
381
480
|
function assertEmptyConfig(config: unknown): asserts config is ActorProcessorConfig {
|
|
382
|
-
|
|
383
|
-
throw new ActorAddressingError('registration-mismatch', 'Processor config must be an empty object');
|
|
384
|
-
}
|
|
481
|
+
readExactRecord(config, [], 'registration-mismatch', 'Processor config');
|
|
385
482
|
}
|
|
386
483
|
|
|
387
484
|
function runtimeState<TInvocation, TOutput>(
|
|
@@ -399,32 +496,60 @@ function runtimeState<TInvocation, TOutput>(
|
|
|
399
496
|
return state;
|
|
400
497
|
}
|
|
401
498
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
499
|
+
function createRuntimeNonce(): string {
|
|
500
|
+
runtimeNonceSequence += 1;
|
|
501
|
+
const randomPart =
|
|
502
|
+
typeof globalThis.crypto?.randomUUID === 'function'
|
|
503
|
+
? globalThis.crypto.randomUUID().replaceAll('-', '')
|
|
504
|
+
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2) || '0'}`;
|
|
505
|
+
return `runtime-${randomPart}-${runtimeNonceSequence.toString(36)}`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function beginMutation<TInvocation, TOutput>(
|
|
509
|
+
state: LocalActorAddressingState<TInvocation, TOutput>,
|
|
510
|
+
operation: string,
|
|
511
|
+
): number {
|
|
512
|
+
if (state.mutationGuard !== undefined) {
|
|
410
513
|
throw new ActorAddressingError(
|
|
411
514
|
'registration-mismatch',
|
|
412
|
-
|
|
515
|
+
`${operation} cannot mutate the runtime while ${state.mutationGuard} is in progress`,
|
|
413
516
|
);
|
|
414
517
|
}
|
|
415
|
-
|
|
518
|
+
state.mutationGuard = operation;
|
|
519
|
+
return state.mutationVersion;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function endMutation<TInvocation, TOutput>(
|
|
523
|
+
state: LocalActorAddressingState<TInvocation, TOutput>,
|
|
524
|
+
operation: string,
|
|
525
|
+
): void {
|
|
526
|
+
if (state.mutationGuard === operation) {
|
|
527
|
+
state.mutationGuard = undefined;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function createLocalActorAddressingRuntime<TInvocation, TOutput>(
|
|
532
|
+
config: LocalActorAddressingRuntimeConfig = {},
|
|
533
|
+
): LocalActorAddressingRuntime<TInvocation, TOutput> {
|
|
534
|
+
const record = readClosedRecord(
|
|
535
|
+
config,
|
|
536
|
+
[],
|
|
537
|
+
RUNTIME_CONFIG_KEYS,
|
|
538
|
+
'registration-mismatch',
|
|
539
|
+
'runtime config',
|
|
540
|
+
);
|
|
541
|
+
const resolveAlias = record.get('resolveAlias');
|
|
542
|
+
const registrationIdFactory = record.get('registrationIdFactory');
|
|
543
|
+
if (resolveAlias !== undefined && typeof resolveAlias !== 'function') {
|
|
416
544
|
throw new ActorAddressingError('registration-mismatch', 'resolveAlias must be a function');
|
|
417
545
|
}
|
|
418
|
-
if (
|
|
419
|
-
config.registrationIdFactory !== undefined &&
|
|
420
|
-
typeof config.registrationIdFactory !== 'function'
|
|
421
|
-
) {
|
|
546
|
+
if (registrationIdFactory !== undefined && typeof registrationIdFactory !== 'function') {
|
|
422
547
|
throw new ActorAddressingError('registration-mismatch', 'registrationIdFactory must be a function');
|
|
423
548
|
}
|
|
424
549
|
|
|
425
550
|
const randomPart = Math.random().toString(36).slice(2) || '0';
|
|
426
551
|
const runtimeInstanceId = parseOpaqueToken(
|
|
427
|
-
|
|
552
|
+
record.get('runtimeInstanceId') ?? `local-runtime-${Date.now().toString(36)}-${randomPart}`,
|
|
428
553
|
'runtimeInstanceId',
|
|
429
554
|
'registration-mismatch',
|
|
430
555
|
);
|
|
@@ -434,12 +559,15 @@ export function createLocalActorAddressingRuntime<TInvocation, TOutput>(
|
|
|
434
559
|
>;
|
|
435
560
|
runtimeStates.set(runtime, {
|
|
436
561
|
registrations: new Map(),
|
|
437
|
-
|
|
438
|
-
|
|
562
|
+
usedRegistrationIds: new Set(),
|
|
563
|
+
runtimeNonce: createRuntimeNonce(),
|
|
564
|
+
resolveAlias: resolveAlias as LocalActorAddressingRuntimeConfig['resolveAlias'],
|
|
439
565
|
registrationIdFactory:
|
|
440
|
-
|
|
566
|
+
(registrationIdFactory as LocalActorAddressingRuntimeConfig['registrationIdFactory']) ??
|
|
441
567
|
((input) => `${input.runtimeInstanceId}:registration-${input.sequence}`),
|
|
442
568
|
sequence: 0,
|
|
569
|
+
mutationVersion: 0,
|
|
570
|
+
mutationGuard: undefined,
|
|
443
571
|
} as LocalActorAddressingState<unknown, unknown>);
|
|
444
572
|
return runtime;
|
|
445
573
|
}
|
|
@@ -447,29 +575,40 @@ export function createLocalActorAddressingRuntime<TInvocation, TOutput>(
|
|
|
447
575
|
function parseEndpoint<TInvocation, TOutput>(
|
|
448
576
|
value: unknown,
|
|
449
577
|
): LocalActorEndpoint<TInvocation, TOutput> {
|
|
450
|
-
|
|
578
|
+
const record = readExactRecord(
|
|
579
|
+
value,
|
|
580
|
+
ENDPOINT_KEYS,
|
|
581
|
+
'registration-mismatch',
|
|
582
|
+
'LocalActorEndpoint',
|
|
583
|
+
);
|
|
451
584
|
const handlerIdentity = parseOpaqueToken(
|
|
452
|
-
|
|
585
|
+
record.get('handlerIdentity'),
|
|
453
586
|
'handlerIdentity',
|
|
454
587
|
'registration-mismatch',
|
|
455
588
|
);
|
|
456
|
-
|
|
589
|
+
const dispatch = record.get('dispatch');
|
|
590
|
+
if (typeof dispatch !== 'function') {
|
|
457
591
|
throw new ActorAddressingError('registration-mismatch', 'endpoint.dispatch must be a function');
|
|
458
592
|
}
|
|
459
593
|
return Object.freeze({
|
|
460
594
|
handlerIdentity,
|
|
461
|
-
dispatch:
|
|
595
|
+
dispatch: dispatch as LocalActorEndpoint<TInvocation, TOutput>['dispatch'],
|
|
462
596
|
});
|
|
463
597
|
}
|
|
464
598
|
|
|
465
599
|
function parseRegisterInput<TInvocation, TOutput>(
|
|
466
600
|
input: unknown,
|
|
467
601
|
): RegisterActorInput<TInvocation, TOutput> {
|
|
468
|
-
|
|
602
|
+
const record = readExactRecord(
|
|
603
|
+
input,
|
|
604
|
+
REGISTER_INPUT_KEYS,
|
|
605
|
+
'registration-mismatch',
|
|
606
|
+
'RegisterActorInput',
|
|
607
|
+
);
|
|
469
608
|
return {
|
|
470
|
-
address: parseActorAddress(
|
|
471
|
-
ownerSnapshot: parseActorOwnerSnapshot(
|
|
472
|
-
endpoint: parseEndpoint<TInvocation, TOutput>(
|
|
609
|
+
address: parseActorAddress(record.get('address')),
|
|
610
|
+
ownerSnapshot: parseActorOwnerSnapshot(record.get('ownerSnapshot')),
|
|
611
|
+
endpoint: parseEndpoint<TInvocation, TOutput>(record.get('endpoint')),
|
|
473
612
|
};
|
|
474
613
|
}
|
|
475
614
|
|
|
@@ -513,16 +652,17 @@ function resolveRegistration<TInvocation, TOutput>(
|
|
|
513
652
|
}
|
|
514
653
|
|
|
515
654
|
function expectedRegistrationId(value: unknown, required: boolean): string | undefined {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
655
|
+
const record = readClosedRecord(
|
|
656
|
+
value,
|
|
657
|
+
required ? EXPECTED_REGISTRATION_ID_KEYS : [],
|
|
658
|
+
required ? [] : EXPECTED_REGISTRATION_ID_KEYS,
|
|
659
|
+
'stale-registration',
|
|
660
|
+
'invocation',
|
|
661
|
+
);
|
|
662
|
+
const valueToParse = record.get('expectedRegistrationId');
|
|
663
|
+
if (valueToParse === undefined && !required) return undefined;
|
|
524
664
|
return parseOpaqueToken(
|
|
525
|
-
|
|
665
|
+
valueToParse,
|
|
526
666
|
'expectedRegistrationId',
|
|
527
667
|
'stale-registration',
|
|
528
668
|
);
|
|
@@ -540,59 +680,105 @@ function assertCurrentRegistration(
|
|
|
540
680
|
}
|
|
541
681
|
}
|
|
542
682
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
): ActorRegistrationReceipt {
|
|
548
|
-
assertEmptyConfig(config);
|
|
549
|
-
const parsed = parseRegisterInput<TInvocation, TOutput>(input);
|
|
550
|
-
const state = runtimeState(runtime);
|
|
551
|
-
const key = actorAddressKey(parsed.address);
|
|
552
|
-
if (state.registrations.has(key)) {
|
|
553
|
-
throw new ActorAddressingError('already-registered', `Actor is already registered: ${key}`);
|
|
554
|
-
}
|
|
555
|
-
const ownerEntry = parsed.ownerSnapshot.registrations.find(
|
|
556
|
-
(entry) => actorAddressKey(entry.address) === key,
|
|
557
|
-
);
|
|
558
|
-
if (!ownerEntry || ownerEntry.handlerIdentity !== parsed.endpoint.handlerIdentity) {
|
|
559
|
-
throw new ActorAddressingError(
|
|
560
|
-
'registration-mismatch',
|
|
561
|
-
'Address and handler identity must be authorized by the owner snapshot',
|
|
562
|
-
);
|
|
683
|
+
function hashRegistrationIdComponent(value: string): string {
|
|
684
|
+
let hash = 0x811c9dc5;
|
|
685
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
686
|
+
hash = Math.imul(hash ^ value.charCodeAt(index), 0x01000193);
|
|
563
687
|
}
|
|
564
|
-
|
|
565
|
-
|
|
688
|
+
return (hash >>> 0).toString(36);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function mintRegistrationId<TInvocation, TOutput>(
|
|
692
|
+
runtime: LocalActorAddressingRuntime<TInvocation, TOutput>,
|
|
693
|
+
state: LocalActorAddressingState<TInvocation, TOutput>,
|
|
694
|
+
sequence: number,
|
|
695
|
+
address: ActorAddress,
|
|
696
|
+
additionallyUsed: ReadonlySet<string> = new Set(),
|
|
697
|
+
): string {
|
|
698
|
+
const factoryValue = parseOpaqueToken(
|
|
566
699
|
state.registrationIdFactory({
|
|
567
700
|
runtimeInstanceId: runtime.runtimeInstanceId,
|
|
568
701
|
sequence,
|
|
569
|
-
address
|
|
702
|
+
address,
|
|
570
703
|
}),
|
|
704
|
+
'registrationIdFactory result',
|
|
705
|
+
'registration-mismatch',
|
|
706
|
+
);
|
|
707
|
+
const factoryComponent =
|
|
708
|
+
factoryValue.length <= 128
|
|
709
|
+
? factoryValue
|
|
710
|
+
: `factory-${hashRegistrationIdComponent(factoryValue)}`;
|
|
711
|
+
const registrationId = parseOpaqueToken(
|
|
712
|
+
`${factoryComponent}:${state.runtimeNonce}:${sequence.toString(36)}`,
|
|
571
713
|
'registrationId',
|
|
572
714
|
'registration-mismatch',
|
|
573
715
|
);
|
|
574
|
-
if (state.
|
|
716
|
+
if (state.usedRegistrationIds.has(registrationId) || additionallyUsed.has(registrationId)) {
|
|
575
717
|
throw new ActorAddressingError(
|
|
576
718
|
'registration-mismatch',
|
|
577
|
-
`registrationIdFactory
|
|
719
|
+
`registrationIdFactory produced an already-used registration id: ${registrationId}`,
|
|
578
720
|
);
|
|
579
721
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
state
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
722
|
+
return registrationId;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
export function registerActor<TInvocation, TOutput>(
|
|
726
|
+
runtime: LocalActorAddressingRuntime<TInvocation, TOutput>,
|
|
727
|
+
input: RegisterActorInput<TInvocation, TOutput>,
|
|
728
|
+
config: ActorProcessorConfig,
|
|
729
|
+
): ActorRegistrationReceipt {
|
|
730
|
+
assertEmptyConfig(config);
|
|
731
|
+
const state = runtimeState(runtime);
|
|
732
|
+
const operation = 'registerActor';
|
|
733
|
+
const startingVersion = beginMutation(state, operation);
|
|
734
|
+
try {
|
|
735
|
+
const parsed = parseRegisterInput<TInvocation, TOutput>(input);
|
|
736
|
+
const key = actorAddressKey(parsed.address);
|
|
737
|
+
if (state.registrations.has(key)) {
|
|
738
|
+
throw new ActorAddressingError('already-registered', `Actor is already registered: ${key}`);
|
|
739
|
+
}
|
|
740
|
+
const ownerEntry = parsed.ownerSnapshot.registrations.find(
|
|
741
|
+
(entry) => actorAddressKey(entry.address) === key,
|
|
742
|
+
);
|
|
743
|
+
if (!ownerEntry || ownerEntry.handlerIdentity !== parsed.endpoint.handlerIdentity) {
|
|
744
|
+
throw new ActorAddressingError(
|
|
745
|
+
'registration-mismatch',
|
|
746
|
+
'Address and handler identity must be authorized by the owner snapshot',
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
const sequence = state.sequence + 1;
|
|
750
|
+
const registrationId = mintRegistrationId(
|
|
751
|
+
runtime,
|
|
752
|
+
state,
|
|
753
|
+
sequence,
|
|
754
|
+
parsed.address,
|
|
755
|
+
);
|
|
756
|
+
if (state.mutationVersion !== startingVersion) {
|
|
757
|
+
throw new ActorAddressingError(
|
|
758
|
+
'registration-mismatch',
|
|
759
|
+
'Runtime changed while registration was being constructed',
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
const receipt: ActorRegistrationReceipt = Object.freeze({
|
|
763
|
+
schemaVersion: ACTOR_REGISTRATION_SCHEMA_VERSION,
|
|
764
|
+
address: parsed.address,
|
|
765
|
+
ownerRevision: parsed.ownerSnapshot.ownerRevision,
|
|
766
|
+
snapshotReceipt: parsed.ownerSnapshot.snapshotReceipt,
|
|
767
|
+
handlerIdentity: parsed.endpoint.handlerIdentity,
|
|
768
|
+
registrationId,
|
|
769
|
+
});
|
|
770
|
+
state.sequence = sequence;
|
|
771
|
+
state.registrations.set(key, {
|
|
772
|
+
address: parsed.address,
|
|
773
|
+
receipt,
|
|
774
|
+
endpoint: parsed.endpoint,
|
|
775
|
+
});
|
|
776
|
+
state.usedRegistrationIds.add(registrationId);
|
|
777
|
+
state.mutationVersion = startingVersion + 1;
|
|
778
|
+
return receipt;
|
|
779
|
+
} finally {
|
|
780
|
+
endMutation(state, operation);
|
|
781
|
+
}
|
|
596
782
|
}
|
|
597
783
|
|
|
598
784
|
export function resolveActor<TInvocation, TOutput>(
|
|
@@ -618,27 +804,25 @@ export async function dispatchActor<TInvocation, TOutput>(
|
|
|
618
804
|
config: ActorProcessorConfig,
|
|
619
805
|
): Promise<TOutput> {
|
|
620
806
|
assertEmptyConfig(config);
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
'dispatch invocation must contain input and optional expectedRegistrationId',
|
|
629
|
-
);
|
|
630
|
-
}
|
|
807
|
+
const invocationRecord = readClosedRecord(
|
|
808
|
+
invocation,
|
|
809
|
+
DISPATCH_INVOCATION_REQUIRED_KEYS,
|
|
810
|
+
EXPECTED_REGISTRATION_ID_KEYS,
|
|
811
|
+
'stale-registration',
|
|
812
|
+
'dispatch invocation',
|
|
813
|
+
);
|
|
631
814
|
const registration = resolveRegistration(runtime, selector);
|
|
815
|
+
const expectedRegistrationIdValue = invocationRecord.get('expectedRegistrationId');
|
|
632
816
|
const expectedId =
|
|
633
|
-
|
|
817
|
+
expectedRegistrationIdValue === undefined
|
|
634
818
|
? undefined
|
|
635
819
|
: parseOpaqueToken(
|
|
636
|
-
|
|
820
|
+
expectedRegistrationIdValue,
|
|
637
821
|
'expectedRegistrationId',
|
|
638
822
|
'stale-registration',
|
|
639
823
|
);
|
|
640
824
|
assertCurrentRegistration(registration.receipt, expectedId);
|
|
641
|
-
return await registration.endpoint.dispatch(
|
|
825
|
+
return await registration.endpoint.dispatch(invocationRecord.get('input') as TInvocation);
|
|
642
826
|
}
|
|
643
827
|
|
|
644
828
|
export function unregisterActor<TInvocation, TOutput>(
|
|
@@ -649,11 +833,24 @@ export function unregisterActor<TInvocation, TOutput>(
|
|
|
649
833
|
): ActorRegistrationReceipt {
|
|
650
834
|
assertEmptyConfig(config);
|
|
651
835
|
const expectedId = expectedRegistrationId(invocation, true);
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
836
|
+
const state = runtimeState(runtime);
|
|
837
|
+
const operation = 'unregisterActor';
|
|
838
|
+
const startingVersion = beginMutation(state, operation);
|
|
839
|
+
try {
|
|
840
|
+
const registration = resolveRegistration(runtime, selector);
|
|
841
|
+
assertCurrentRegistration(registration.receipt, expectedId);
|
|
842
|
+
if (state.mutationVersion !== startingVersion) {
|
|
843
|
+
throw new ActorAddressingError(
|
|
844
|
+
'registration-mismatch',
|
|
845
|
+
'Runtime changed while unregister was being resolved',
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
state.registrations.delete(actorAddressKey(registration.address));
|
|
849
|
+
state.mutationVersion = startingVersion + 1;
|
|
850
|
+
return registration.receipt;
|
|
851
|
+
} finally {
|
|
852
|
+
endMutation(state, operation);
|
|
853
|
+
}
|
|
657
854
|
}
|
|
658
855
|
|
|
659
856
|
export function rebuildActorRegistrations<TInvocation, TOutput>(
|
|
@@ -662,78 +859,101 @@ export function rebuildActorRegistrations<TInvocation, TOutput>(
|
|
|
662
859
|
config: RebuildActorRegistrationsConfig<TInvocation, TOutput>,
|
|
663
860
|
): readonly ActorRegistrationReceipt[] {
|
|
664
861
|
const snapshot = parseActorOwnerSnapshot(input);
|
|
665
|
-
|
|
862
|
+
const configRecord = readExactRecord(
|
|
666
863
|
config,
|
|
667
864
|
['endpointFor'],
|
|
668
865
|
'registration-mismatch',
|
|
669
866
|
'RebuildActorRegistrationsConfig',
|
|
670
867
|
);
|
|
671
|
-
|
|
868
|
+
const endpointFor = configRecord.get('endpointFor');
|
|
869
|
+
if (typeof endpointFor !== 'function') {
|
|
672
870
|
throw new ActorAddressingError('registration-mismatch', 'endpointFor must be a function');
|
|
673
871
|
}
|
|
674
872
|
const state = runtimeState(runtime);
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
873
|
+
const operation = 'rebuildActorRegistrations';
|
|
874
|
+
const startingVersion = beginMutation(state, operation);
|
|
875
|
+
try {
|
|
876
|
+
if (state.registrations.size !== 0) {
|
|
877
|
+
throw new ActorAddressingError(
|
|
878
|
+
'already-registered',
|
|
879
|
+
'Registration recovery requires a fresh empty local runtime',
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const endpoints = snapshot.registrations.map((registration) => {
|
|
884
|
+
const endpoint = parseEndpoint<TInvocation, TOutput>(
|
|
885
|
+
endpointFor(registration) as LocalActorEndpoint<TInvocation, TOutput>,
|
|
886
|
+
);
|
|
887
|
+
if (endpoint.handlerIdentity !== registration.handlerIdentity) {
|
|
888
|
+
throw new ActorAddressingError(
|
|
889
|
+
'registration-mismatch',
|
|
890
|
+
`Recovered endpoint identity does not match snapshot for ${actorAddressKey(registration.address)}`,
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
return endpoint;
|
|
894
|
+
});
|
|
681
895
|
|
|
682
|
-
|
|
683
|
-
const endpoint = parseEndpoint<TInvocation, TOutput>(config.endpointFor(registration));
|
|
684
|
-
if (endpoint.handlerIdentity !== registration.handlerIdentity) {
|
|
896
|
+
if (state.mutationVersion !== startingVersion || state.registrations.size !== 0) {
|
|
685
897
|
throw new ActorAddressingError(
|
|
686
898
|
'registration-mismatch',
|
|
687
|
-
|
|
899
|
+
'Runtime changed while recovery endpoints were being constructed',
|
|
688
900
|
);
|
|
689
901
|
}
|
|
690
|
-
return endpoint;
|
|
691
|
-
});
|
|
692
902
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
903
|
+
let nextSequence = state.sequence;
|
|
904
|
+
const plannedIds = new Set<string>();
|
|
905
|
+
const planned = snapshot.registrations.map((registration, index) => {
|
|
906
|
+
nextSequence += 1;
|
|
907
|
+
const registrationId = mintRegistrationId(
|
|
908
|
+
runtime,
|
|
909
|
+
state,
|
|
910
|
+
nextSequence,
|
|
911
|
+
registration.address,
|
|
912
|
+
plannedIds,
|
|
913
|
+
);
|
|
914
|
+
plannedIds.add(registrationId);
|
|
915
|
+
const receipt: ActorRegistrationReceipt = Object.freeze({
|
|
916
|
+
schemaVersion: ACTOR_REGISTRATION_SCHEMA_VERSION,
|
|
701
917
|
address: registration.address,
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
918
|
+
ownerRevision: snapshot.ownerRevision,
|
|
919
|
+
snapshotReceipt: snapshot.snapshotReceipt,
|
|
920
|
+
handlerIdentity: registration.handlerIdentity,
|
|
921
|
+
registrationId,
|
|
922
|
+
});
|
|
923
|
+
return {
|
|
924
|
+
key: actorAddressKey(registration.address),
|
|
925
|
+
registration: {
|
|
926
|
+
address: registration.address,
|
|
927
|
+
receipt,
|
|
928
|
+
endpoint: endpoints[index]!,
|
|
929
|
+
},
|
|
930
|
+
};
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
if (state.mutationVersion !== startingVersion || state.registrations.size !== 0) {
|
|
707
934
|
throw new ActorAddressingError(
|
|
708
935
|
'registration-mismatch',
|
|
709
|
-
|
|
936
|
+
'Runtime changed while recovery was being planned',
|
|
710
937
|
);
|
|
711
938
|
}
|
|
712
|
-
plannedIds.add(registrationId);
|
|
713
|
-
const receipt: ActorRegistrationReceipt = Object.freeze({
|
|
714
|
-
schemaVersion: ACTOR_REGISTRATION_SCHEMA_VERSION,
|
|
715
|
-
address: registration.address,
|
|
716
|
-
ownerRevision: snapshot.ownerRevision,
|
|
717
|
-
snapshotReceipt: snapshot.snapshotReceipt,
|
|
718
|
-
handlerIdentity: registration.handlerIdentity,
|
|
719
|
-
registrationId,
|
|
720
|
-
});
|
|
721
|
-
return {
|
|
722
|
-
key: actorAddressKey(registration.address),
|
|
723
|
-
registration: {
|
|
724
|
-
address: registration.address,
|
|
725
|
-
receipt,
|
|
726
|
-
endpoint: endpoints[index]!,
|
|
727
|
-
},
|
|
728
|
-
};
|
|
729
|
-
});
|
|
730
939
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
940
|
+
const nextRegistrations = new Map(
|
|
941
|
+
planned.map((item) => [item.key, item.registration] as const),
|
|
942
|
+
);
|
|
943
|
+
const nextUsedRegistrationIds = new Set(state.usedRegistrationIds);
|
|
944
|
+
for (const registrationId of plannedIds) {
|
|
945
|
+
nextUsedRegistrationIds.add(registrationId);
|
|
946
|
+
}
|
|
947
|
+
const receipts = Object.freeze(planned.map((item) => item.registration.receipt));
|
|
948
|
+
|
|
949
|
+
state.registrations = nextRegistrations;
|
|
950
|
+
state.usedRegistrationIds = nextUsedRegistrationIds;
|
|
951
|
+
state.sequence = nextSequence;
|
|
952
|
+
state.mutationVersion = startingVersion + 1;
|
|
953
|
+
return receipts;
|
|
954
|
+
} finally {
|
|
955
|
+
endMutation(state, operation);
|
|
734
956
|
}
|
|
735
|
-
state.sequence = nextSequence;
|
|
736
|
-
return Object.freeze(planned.map((item) => item.registration.receipt));
|
|
737
957
|
}
|
|
738
958
|
|
|
739
959
|
export function createActorRefEndpoint<
|