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.
@@ -49,22 +49,80 @@ const REGISTRATION_RECEIPT_KEYS = [
49
49
  ];
50
50
  const REGISTER_INPUT_KEYS = ['address', 'ownerSnapshot', 'endpoint'];
51
51
  const ENDPOINT_KEYS = ['handlerIdentity', 'dispatch'];
52
+ const RUNTIME_CONFIG_KEYS = ['runtimeInstanceId', 'resolveAlias', 'registrationIdFactory'];
53
+ const EXPECTED_REGISTRATION_ID_KEYS = ['expectedRegistrationId'];
54
+ const DISPATCH_INVOCATION_REQUIRED_KEYS = ['input'];
52
55
  const ADDRESS_SEGMENT = /^[A-Za-z0-9](?:[A-Za-z0-9._~-]{0,126}[A-Za-z0-9])?$/;
53
56
  const OPAQUE_TOKEN = /^[A-Za-z0-9](?:[A-Za-z0-9._~:/-]{0,254}[A-Za-z0-9])?$/;
54
57
  const runtimeStates = new WeakMap();
55
- function isRecord(value) {
56
- return typeof value === 'object' && value !== null && !Array.isArray(value);
58
+ let runtimeNonceSequence = 0;
59
+ function closedRecordError(code, label, detail) {
60
+ return new ActorAddressingError(code, `${label} ${detail}`);
57
61
  }
58
- function assertExactRecord(value, keys, code, label) {
59
- if (!isRecord(value)) {
60
- throw new ActorAddressingError(code, `${label} must be an object`);
62
+ function readClosedRecord(value, requiredKeys, optionalKeys, code, label) {
63
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
64
+ throw closedRecordError(code, label, 'must be an object');
65
+ }
66
+ const prototype = Reflect.getPrototypeOf(value);
67
+ if (prototype !== Object.prototype && prototype !== null) {
68
+ throw closedRecordError(code, label, 'must use Object.prototype or a null prototype');
69
+ }
70
+ const allowedKeys = new Set([...requiredKeys, ...optionalKeys]);
71
+ const values = new Map();
72
+ for (const key of Reflect.ownKeys(value)) {
73
+ if (typeof key !== 'string' || !allowedKeys.has(key)) {
74
+ throw closedRecordError(code, label, `must contain only: ${[...allowedKeys].sort().join(', ')}`);
75
+ }
76
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
77
+ if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
78
+ throw closedRecordError(code, label, `${key} must be an own enumerable data property`);
79
+ }
80
+ values.set(key, descriptor.value);
81
+ }
82
+ for (const key of requiredKeys) {
83
+ if (!values.has(key)) {
84
+ throw closedRecordError(code, label, `must contain required field: ${key}`);
85
+ }
61
86
  }
62
- const actualKeys = Object.keys(value).sort();
63
- const expectedKeys = [...keys].sort();
64
- if (actualKeys.length !== expectedKeys.length ||
65
- actualKeys.some((key, index) => key !== expectedKeys[index])) {
66
- throw new ActorAddressingError(code, `${label} must contain only: ${expectedKeys.join(', ')}`);
87
+ return values;
88
+ }
89
+ function readExactRecord(value, keys, code, label) {
90
+ return readClosedRecord(value, keys, [], code, label);
91
+ }
92
+ function readClosedArray(value, code, label) {
93
+ if (!Array.isArray(value) || Reflect.getPrototypeOf(value) !== Array.prototype) {
94
+ throw closedRecordError(code, label, 'must be a plain array');
95
+ }
96
+ const lengthDescriptor = Reflect.getOwnPropertyDescriptor(value, 'length');
97
+ const lengthValue = lengthDescriptor && 'value' in lengthDescriptor ? lengthDescriptor.value : undefined;
98
+ if (!lengthDescriptor ||
99
+ !('value' in lengthDescriptor) ||
100
+ lengthDescriptor.enumerable ||
101
+ typeof lengthValue !== 'number' ||
102
+ !Number.isSafeInteger(lengthValue) ||
103
+ lengthValue < 0) {
104
+ throw closedRecordError(code, label, 'must have the standard own data length property');
105
+ }
106
+ const length = lengthValue;
107
+ const expectedKeys = new Set(['length']);
108
+ for (let index = 0; index < length; index += 1) {
109
+ expectedKeys.add(String(index));
110
+ }
111
+ const ownKeys = Reflect.ownKeys(value);
112
+ if (ownKeys.length !== expectedKeys.size ||
113
+ ownKeys.some((key) => typeof key !== 'string' || !expectedKeys.has(key))) {
114
+ throw closedRecordError(code, label, 'must be dense and contain no extra own keys');
115
+ }
116
+ const elements = [];
117
+ for (let index = 0; index < length; index += 1) {
118
+ const key = String(index);
119
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
120
+ if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
121
+ throw closedRecordError(code, label, `${key} must be an own enumerable data property`);
122
+ }
123
+ elements.push(descriptor.value);
67
124
  }
125
+ return elements;
68
126
  }
69
127
  function parseAddressSegment(value, field, code = 'invalid-address') {
70
128
  if (typeof value !== 'string' ||
@@ -86,16 +144,16 @@ function parseOpaqueToken(value, field, code) {
86
144
  return value;
87
145
  }
88
146
  function parseActorAddress(value) {
89
- assertExactRecord(value, ADDRESS_KEYS, 'invalid-address', 'ActorAddress');
90
- if (value.schemaVersion !== exports.ACTOR_ADDRESS_SCHEMA_VERSION) {
147
+ const record = readExactRecord(value, ADDRESS_KEYS, 'invalid-address', 'ActorAddress');
148
+ if (record.get('schemaVersion') !== exports.ACTOR_ADDRESS_SCHEMA_VERSION) {
91
149
  throw new ActorAddressingError('invalid-address', 'Unsupported ActorAddress schemaVersion');
92
150
  }
93
151
  return Object.freeze({
94
152
  schemaVersion: exports.ACTOR_ADDRESS_SCHEMA_VERSION,
95
- namespace: parseAddressSegment(value.namespace, 'namespace'),
96
- deploymentId: parseAddressSegment(value.deploymentId, 'deploymentId'),
97
- actorKind: parseAddressSegment(value.actorKind, 'actorKind'),
98
- logicalKey: parseAddressSegment(value.logicalKey, 'logicalKey'),
153
+ namespace: parseAddressSegment(record.get('namespace'), 'namespace'),
154
+ deploymentId: parseAddressSegment(record.get('deploymentId'), 'deploymentId'),
155
+ actorKind: parseAddressSegment(record.get('actorKind'), 'actorKind'),
156
+ logicalKey: parseAddressSegment(record.get('logicalKey'), 'logicalKey'),
99
157
  });
100
158
  }
101
159
  function actorAddressKey(address) {
@@ -109,16 +167,13 @@ function actorAddressKey(address) {
109
167
  ].join(':');
110
168
  }
111
169
  function parseActorSelector(value) {
112
- if (!isRecord(value)) {
113
- throw new ActorAddressingError('invalid-selector', 'ActorSelector must be an object');
114
- }
115
- const keys = Object.keys(value);
116
- if (keys.length !== 1 || (keys[0] !== 'byAddress' && keys[0] !== 'byAlias')) {
170
+ const record = readClosedRecord(value, [], ['byAddress', 'byAlias'], 'invalid-selector', 'ActorSelector');
171
+ if (record.size !== 1) {
117
172
  throw new ActorAddressingError('invalid-selector', 'ActorSelector must contain exactly one of byAddress or byAlias');
118
173
  }
119
- if (keys[0] === 'byAddress') {
174
+ if (record.has('byAddress')) {
120
175
  try {
121
- return Object.freeze({ byAddress: parseActorAddress(value.byAddress) });
176
+ return Object.freeze({ byAddress: parseActorAddress(record.get('byAddress')) });
122
177
  }
123
178
  catch (error) {
124
179
  if (error instanceof ActorAddressingError) {
@@ -128,24 +183,22 @@ function parseActorSelector(value) {
128
183
  }
129
184
  }
130
185
  return Object.freeze({
131
- byAlias: parseOpaqueToken(value.byAlias, 'byAlias', 'invalid-selector'),
186
+ byAlias: parseOpaqueToken(record.get('byAlias'), 'byAlias', 'invalid-selector'),
132
187
  });
133
188
  }
134
189
  function parseActorOwnerSnapshot(value) {
135
- assertExactRecord(value, OWNER_SNAPSHOT_KEYS, 'invalid-owner-snapshot', 'ActorOwnerSnapshot');
136
- if (value.schemaVersion !== exports.ACTOR_OWNER_SNAPSHOT_SCHEMA_VERSION) {
190
+ const record = readExactRecord(value, OWNER_SNAPSHOT_KEYS, 'invalid-owner-snapshot', 'ActorOwnerSnapshot');
191
+ if (record.get('schemaVersion') !== exports.ACTOR_OWNER_SNAPSHOT_SCHEMA_VERSION) {
137
192
  throw new ActorAddressingError('invalid-owner-snapshot', 'Unsupported owner snapshot schemaVersion');
138
193
  }
139
- if (!Array.isArray(value.registrations)) {
140
- throw new ActorAddressingError('invalid-owner-snapshot', 'registrations must be an array');
141
- }
194
+ const sourceRegistrations = readClosedArray(record.get('registrations'), 'invalid-owner-snapshot', 'registrations');
142
195
  const registrations = [];
143
196
  const seen = new Set();
144
- for (const registration of value.registrations) {
145
- assertExactRecord(registration, OWNER_REGISTRATION_KEYS, 'invalid-owner-snapshot', 'owner snapshot registration');
197
+ for (const registration of sourceRegistrations) {
198
+ const registrationRecord = readExactRecord(registration, OWNER_REGISTRATION_KEYS, 'invalid-owner-snapshot', 'owner snapshot registration');
146
199
  let address;
147
200
  try {
148
- address = parseActorAddress(registration.address);
201
+ address = parseActorAddress(registrationRecord.get('address'));
149
202
  }
150
203
  catch (error) {
151
204
  if (error instanceof ActorAddressingError) {
@@ -160,34 +213,32 @@ function parseActorOwnerSnapshot(value) {
160
213
  seen.add(key);
161
214
  registrations.push(Object.freeze({
162
215
  address,
163
- handlerIdentity: parseOpaqueToken(registration.handlerIdentity, 'handlerIdentity', 'invalid-owner-snapshot'),
216
+ handlerIdentity: parseOpaqueToken(registrationRecord.get('handlerIdentity'), 'handlerIdentity', 'invalid-owner-snapshot'),
164
217
  }));
165
218
  }
166
219
  return Object.freeze({
167
220
  schemaVersion: exports.ACTOR_OWNER_SNAPSHOT_SCHEMA_VERSION,
168
- ownerRevision: parseOpaqueToken(value.ownerRevision, 'ownerRevision', 'invalid-owner-snapshot'),
169
- snapshotReceipt: parseOpaqueToken(value.snapshotReceipt, 'snapshotReceipt', 'invalid-owner-snapshot'),
221
+ ownerRevision: parseOpaqueToken(record.get('ownerRevision'), 'ownerRevision', 'invalid-owner-snapshot'),
222
+ snapshotReceipt: parseOpaqueToken(record.get('snapshotReceipt'), 'snapshotReceipt', 'invalid-owner-snapshot'),
170
223
  registrations: Object.freeze(registrations),
171
224
  });
172
225
  }
173
226
  function parseActorRegistrationReceipt(value) {
174
- assertExactRecord(value, REGISTRATION_RECEIPT_KEYS, 'registration-mismatch', 'ActorRegistrationReceipt');
175
- if (value.schemaVersion !== exports.ACTOR_REGISTRATION_SCHEMA_VERSION) {
227
+ const record = readExactRecord(value, REGISTRATION_RECEIPT_KEYS, 'registration-mismatch', 'ActorRegistrationReceipt');
228
+ if (record.get('schemaVersion') !== exports.ACTOR_REGISTRATION_SCHEMA_VERSION) {
176
229
  throw new ActorAddressingError('registration-mismatch', 'Unsupported registration schemaVersion');
177
230
  }
178
231
  return Object.freeze({
179
232
  schemaVersion: exports.ACTOR_REGISTRATION_SCHEMA_VERSION,
180
- address: parseActorAddress(value.address),
181
- ownerRevision: parseOpaqueToken(value.ownerRevision, 'ownerRevision', 'registration-mismatch'),
182
- snapshotReceipt: parseOpaqueToken(value.snapshotReceipt, 'snapshotReceipt', 'registration-mismatch'),
183
- handlerIdentity: parseOpaqueToken(value.handlerIdentity, 'handlerIdentity', 'registration-mismatch'),
184
- registrationId: parseOpaqueToken(value.registrationId, 'registrationId', 'registration-mismatch'),
233
+ address: parseActorAddress(record.get('address')),
234
+ ownerRevision: parseOpaqueToken(record.get('ownerRevision'), 'ownerRevision', 'registration-mismatch'),
235
+ snapshotReceipt: parseOpaqueToken(record.get('snapshotReceipt'), 'snapshotReceipt', 'registration-mismatch'),
236
+ handlerIdentity: parseOpaqueToken(record.get('handlerIdentity'), 'handlerIdentity', 'registration-mismatch'),
237
+ registrationId: parseOpaqueToken(record.get('registrationId'), 'registrationId', 'registration-mismatch'),
185
238
  });
186
239
  }
187
240
  function assertEmptyConfig(config) {
188
- if (!isRecord(config) || Object.keys(config).length !== 0) {
189
- throw new ActorAddressingError('registration-mismatch', 'Processor config must be an empty object');
190
- }
241
+ readExactRecord(config, [], 'registration-mismatch', 'Processor config');
191
242
  }
192
243
  function runtimeState(runtime) {
193
244
  const state = runtimeStates.get(runtime);
@@ -196,51 +247,69 @@ function runtimeState(runtime) {
196
247
  }
197
248
  return state;
198
249
  }
199
- function createLocalActorAddressingRuntime(config = {}) {
200
- if (!isRecord(config)) {
201
- throw new ActorAddressingError('registration-mismatch', 'runtime config must be an object');
250
+ function createRuntimeNonce() {
251
+ runtimeNonceSequence += 1;
252
+ const randomPart = typeof globalThis.crypto?.randomUUID === 'function'
253
+ ? globalThis.crypto.randomUUID().replaceAll('-', '')
254
+ : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2) || '0'}`;
255
+ return `runtime-${randomPart}-${runtimeNonceSequence.toString(36)}`;
256
+ }
257
+ function beginMutation(state, operation) {
258
+ if (state.mutationGuard !== undefined) {
259
+ throw new ActorAddressingError('registration-mismatch', `${operation} cannot mutate the runtime while ${state.mutationGuard} is in progress`);
202
260
  }
203
- const allowedKeys = new Set(['runtimeInstanceId', 'resolveAlias', 'registrationIdFactory']);
204
- if (Object.keys(config).some((key) => !allowedKeys.has(key))) {
205
- throw new ActorAddressingError('registration-mismatch', 'runtime config contains an unsupported field');
261
+ state.mutationGuard = operation;
262
+ return state.mutationVersion;
263
+ }
264
+ function endMutation(state, operation) {
265
+ if (state.mutationGuard === operation) {
266
+ state.mutationGuard = undefined;
206
267
  }
207
- if (config.resolveAlias !== undefined && typeof config.resolveAlias !== 'function') {
268
+ }
269
+ function createLocalActorAddressingRuntime(config = {}) {
270
+ const record = readClosedRecord(config, [], RUNTIME_CONFIG_KEYS, 'registration-mismatch', 'runtime config');
271
+ const resolveAlias = record.get('resolveAlias');
272
+ const registrationIdFactory = record.get('registrationIdFactory');
273
+ if (resolveAlias !== undefined && typeof resolveAlias !== 'function') {
208
274
  throw new ActorAddressingError('registration-mismatch', 'resolveAlias must be a function');
209
275
  }
210
- if (config.registrationIdFactory !== undefined &&
211
- typeof config.registrationIdFactory !== 'function') {
276
+ if (registrationIdFactory !== undefined && typeof registrationIdFactory !== 'function') {
212
277
  throw new ActorAddressingError('registration-mismatch', 'registrationIdFactory must be a function');
213
278
  }
214
279
  const randomPart = Math.random().toString(36).slice(2) || '0';
215
- const runtimeInstanceId = parseOpaqueToken(config.runtimeInstanceId ?? `local-runtime-${Date.now().toString(36)}-${randomPart}`, 'runtimeInstanceId', 'registration-mismatch');
280
+ const runtimeInstanceId = parseOpaqueToken(record.get('runtimeInstanceId') ?? `local-runtime-${Date.now().toString(36)}-${randomPart}`, 'runtimeInstanceId', 'registration-mismatch');
216
281
  const runtime = Object.freeze({ runtimeInstanceId });
217
282
  runtimeStates.set(runtime, {
218
283
  registrations: new Map(),
219
- registrationIds: new Set(),
220
- resolveAlias: config.resolveAlias,
221
- registrationIdFactory: config.registrationIdFactory ??
284
+ usedRegistrationIds: new Set(),
285
+ runtimeNonce: createRuntimeNonce(),
286
+ resolveAlias: resolveAlias,
287
+ registrationIdFactory: registrationIdFactory ??
222
288
  ((input) => `${input.runtimeInstanceId}:registration-${input.sequence}`),
223
289
  sequence: 0,
290
+ mutationVersion: 0,
291
+ mutationGuard: undefined,
224
292
  });
225
293
  return runtime;
226
294
  }
227
295
  function parseEndpoint(value) {
228
- assertExactRecord(value, ENDPOINT_KEYS, 'registration-mismatch', 'LocalActorEndpoint');
229
- const handlerIdentity = parseOpaqueToken(value.handlerIdentity, 'handlerIdentity', 'registration-mismatch');
230
- if (typeof value.dispatch !== 'function') {
296
+ const record = readExactRecord(value, ENDPOINT_KEYS, 'registration-mismatch', 'LocalActorEndpoint');
297
+ const handlerIdentity = parseOpaqueToken(record.get('handlerIdentity'), 'handlerIdentity', 'registration-mismatch');
298
+ const dispatch = record.get('dispatch');
299
+ if (typeof dispatch !== 'function') {
231
300
  throw new ActorAddressingError('registration-mismatch', 'endpoint.dispatch must be a function');
232
301
  }
233
302
  return Object.freeze({
234
303
  handlerIdentity,
235
- dispatch: value.dispatch,
304
+ dispatch: dispatch,
236
305
  });
237
306
  }
238
307
  function parseRegisterInput(input) {
239
- assertExactRecord(input, REGISTER_INPUT_KEYS, 'registration-mismatch', 'RegisterActorInput');
308
+ const record = readExactRecord(input, REGISTER_INPUT_KEYS, 'registration-mismatch', 'RegisterActorInput');
240
309
  return {
241
- address: parseActorAddress(input.address),
242
- ownerSnapshot: parseActorOwnerSnapshot(input.ownerSnapshot),
243
- endpoint: parseEndpoint(input.endpoint),
310
+ address: parseActorAddress(record.get('address')),
311
+ ownerSnapshot: parseActorOwnerSnapshot(record.get('ownerSnapshot')),
312
+ endpoint: parseEndpoint(record.get('endpoint')),
244
313
  };
245
314
  }
246
315
  function resolveRegistration(runtime, selector) {
@@ -272,59 +341,80 @@ function resolveRegistration(runtime, selector) {
272
341
  return registration;
273
342
  }
274
343
  function expectedRegistrationId(value, required) {
275
- if (!isRecord(value)) {
276
- throw new ActorAddressingError('stale-registration', 'invocation must be an object');
277
- }
278
- const allowedKeys = required ? ['expectedRegistrationId'] : ['expectedRegistrationId'];
279
- if (Object.keys(value).some((key) => !allowedKeys.includes(key))) {
280
- throw new ActorAddressingError('stale-registration', 'invocation contains an unsupported field');
281
- }
282
- if (value.expectedRegistrationId === undefined && !required)
344
+ const record = readClosedRecord(value, required ? EXPECTED_REGISTRATION_ID_KEYS : [], required ? [] : EXPECTED_REGISTRATION_ID_KEYS, 'stale-registration', 'invocation');
345
+ const valueToParse = record.get('expectedRegistrationId');
346
+ if (valueToParse === undefined && !required)
283
347
  return undefined;
284
- return parseOpaqueToken(value.expectedRegistrationId, 'expectedRegistrationId', 'stale-registration');
348
+ return parseOpaqueToken(valueToParse, 'expectedRegistrationId', 'stale-registration');
285
349
  }
286
350
  function assertCurrentRegistration(registration, expectedId) {
287
351
  if (expectedId !== undefined && expectedId !== registration.registrationId) {
288
352
  throw new ActorAddressingError('stale-registration', `Expected registration ${expectedId} is not current`);
289
353
  }
290
354
  }
355
+ function hashRegistrationIdComponent(value) {
356
+ let hash = 0x811c9dc5;
357
+ for (let index = 0; index < value.length; index += 1) {
358
+ hash = Math.imul(hash ^ value.charCodeAt(index), 0x01000193);
359
+ }
360
+ return (hash >>> 0).toString(36);
361
+ }
362
+ function mintRegistrationId(runtime, state, sequence, address, additionallyUsed = new Set()) {
363
+ const factoryValue = parseOpaqueToken(state.registrationIdFactory({
364
+ runtimeInstanceId: runtime.runtimeInstanceId,
365
+ sequence,
366
+ address,
367
+ }), 'registrationIdFactory result', 'registration-mismatch');
368
+ const factoryComponent = factoryValue.length <= 128
369
+ ? factoryValue
370
+ : `factory-${hashRegistrationIdComponent(factoryValue)}`;
371
+ const registrationId = parseOpaqueToken(`${factoryComponent}:${state.runtimeNonce}:${sequence.toString(36)}`, 'registrationId', 'registration-mismatch');
372
+ if (state.usedRegistrationIds.has(registrationId) || additionallyUsed.has(registrationId)) {
373
+ throw new ActorAddressingError('registration-mismatch', `registrationIdFactory produced an already-used registration id: ${registrationId}`);
374
+ }
375
+ return registrationId;
376
+ }
291
377
  function registerActor(runtime, input, config) {
292
378
  assertEmptyConfig(config);
293
- const parsed = parseRegisterInput(input);
294
379
  const state = runtimeState(runtime);
295
- const key = actorAddressKey(parsed.address);
296
- if (state.registrations.has(key)) {
297
- throw new ActorAddressingError('already-registered', `Actor is already registered: ${key}`);
298
- }
299
- const ownerEntry = parsed.ownerSnapshot.registrations.find((entry) => actorAddressKey(entry.address) === key);
300
- if (!ownerEntry || ownerEntry.handlerIdentity !== parsed.endpoint.handlerIdentity) {
301
- throw new ActorAddressingError('registration-mismatch', 'Address and handler identity must be authorized by the owner snapshot');
380
+ const operation = 'registerActor';
381
+ const startingVersion = beginMutation(state, operation);
382
+ try {
383
+ const parsed = parseRegisterInput(input);
384
+ const key = actorAddressKey(parsed.address);
385
+ if (state.registrations.has(key)) {
386
+ throw new ActorAddressingError('already-registered', `Actor is already registered: ${key}`);
387
+ }
388
+ const ownerEntry = parsed.ownerSnapshot.registrations.find((entry) => actorAddressKey(entry.address) === key);
389
+ if (!ownerEntry || ownerEntry.handlerIdentity !== parsed.endpoint.handlerIdentity) {
390
+ throw new ActorAddressingError('registration-mismatch', 'Address and handler identity must be authorized by the owner snapshot');
391
+ }
392
+ const sequence = state.sequence + 1;
393
+ const registrationId = mintRegistrationId(runtime, state, sequence, parsed.address);
394
+ if (state.mutationVersion !== startingVersion) {
395
+ throw new ActorAddressingError('registration-mismatch', 'Runtime changed while registration was being constructed');
396
+ }
397
+ const receipt = Object.freeze({
398
+ schemaVersion: exports.ACTOR_REGISTRATION_SCHEMA_VERSION,
399
+ address: parsed.address,
400
+ ownerRevision: parsed.ownerSnapshot.ownerRevision,
401
+ snapshotReceipt: parsed.ownerSnapshot.snapshotReceipt,
402
+ handlerIdentity: parsed.endpoint.handlerIdentity,
403
+ registrationId,
404
+ });
405
+ state.sequence = sequence;
406
+ state.registrations.set(key, {
407
+ address: parsed.address,
408
+ receipt,
409
+ endpoint: parsed.endpoint,
410
+ });
411
+ state.usedRegistrationIds.add(registrationId);
412
+ state.mutationVersion = startingVersion + 1;
413
+ return receipt;
302
414
  }
303
- const sequence = state.sequence + 1;
304
- const registrationId = parseOpaqueToken(state.registrationIdFactory({
305
- runtimeInstanceId: runtime.runtimeInstanceId,
306
- sequence,
307
- address: parsed.address,
308
- }), 'registrationId', 'registration-mismatch');
309
- if (state.registrationIds.has(registrationId)) {
310
- throw new ActorAddressingError('registration-mismatch', `registrationIdFactory returned a duplicate id: ${registrationId}`);
415
+ finally {
416
+ endMutation(state, operation);
311
417
  }
312
- state.sequence = sequence;
313
- const receipt = Object.freeze({
314
- schemaVersion: exports.ACTOR_REGISTRATION_SCHEMA_VERSION,
315
- address: parsed.address,
316
- ownerRevision: parsed.ownerSnapshot.ownerRevision,
317
- snapshotReceipt: parsed.ownerSnapshot.snapshotReceipt,
318
- handlerIdentity: parsed.endpoint.handlerIdentity,
319
- registrationId,
320
- });
321
- state.registrations.set(key, {
322
- address: parsed.address,
323
- receipt,
324
- endpoint: parsed.endpoint,
325
- });
326
- state.registrationIds.add(registrationId);
327
- return receipt;
328
418
  }
329
419
  function resolveActor(runtime, selector, invocation, config) {
330
420
  assertEmptyConfig(config);
@@ -338,82 +428,100 @@ function resolveActor(runtime, selector, invocation, config) {
338
428
  }
339
429
  async function dispatchActor(runtime, selector, invocation, config) {
340
430
  assertEmptyConfig(config);
341
- if (!isRecord(invocation)) {
342
- throw new ActorAddressingError('stale-registration', 'dispatch invocation must be an object');
343
- }
344
- const keys = Object.keys(invocation);
345
- if (!keys.includes('input') || keys.some((key) => key !== 'input' && key !== 'expectedRegistrationId')) {
346
- throw new ActorAddressingError('stale-registration', 'dispatch invocation must contain input and optional expectedRegistrationId');
347
- }
431
+ const invocationRecord = readClosedRecord(invocation, DISPATCH_INVOCATION_REQUIRED_KEYS, EXPECTED_REGISTRATION_ID_KEYS, 'stale-registration', 'dispatch invocation');
348
432
  const registration = resolveRegistration(runtime, selector);
349
- const expectedId = invocation.expectedRegistrationId === undefined
433
+ const expectedRegistrationIdValue = invocationRecord.get('expectedRegistrationId');
434
+ const expectedId = expectedRegistrationIdValue === undefined
350
435
  ? undefined
351
- : parseOpaqueToken(invocation.expectedRegistrationId, 'expectedRegistrationId', 'stale-registration');
436
+ : parseOpaqueToken(expectedRegistrationIdValue, 'expectedRegistrationId', 'stale-registration');
352
437
  assertCurrentRegistration(registration.receipt, expectedId);
353
- return await registration.endpoint.dispatch(invocation.input);
438
+ return await registration.endpoint.dispatch(invocationRecord.get('input'));
354
439
  }
355
440
  function unregisterActor(runtime, selector, invocation, config) {
356
441
  assertEmptyConfig(config);
357
442
  const expectedId = expectedRegistrationId(invocation, true);
358
- const registration = resolveRegistration(runtime, selector);
359
- assertCurrentRegistration(registration.receipt, expectedId);
360
- runtimeState(runtime).registrations.delete(actorAddressKey(registration.address));
361
- runtimeState(runtime).registrationIds.delete(registration.receipt.registrationId);
362
- return registration.receipt;
443
+ const state = runtimeState(runtime);
444
+ const operation = 'unregisterActor';
445
+ const startingVersion = beginMutation(state, operation);
446
+ try {
447
+ const registration = resolveRegistration(runtime, selector);
448
+ assertCurrentRegistration(registration.receipt, expectedId);
449
+ if (state.mutationVersion !== startingVersion) {
450
+ throw new ActorAddressingError('registration-mismatch', 'Runtime changed while unregister was being resolved');
451
+ }
452
+ state.registrations.delete(actorAddressKey(registration.address));
453
+ state.mutationVersion = startingVersion + 1;
454
+ return registration.receipt;
455
+ }
456
+ finally {
457
+ endMutation(state, operation);
458
+ }
363
459
  }
364
460
  function rebuildActorRegistrations(runtime, input, config) {
365
461
  const snapshot = parseActorOwnerSnapshot(input);
366
- assertExactRecord(config, ['endpointFor'], 'registration-mismatch', 'RebuildActorRegistrationsConfig');
367
- if (typeof config.endpointFor !== 'function') {
462
+ const configRecord = readExactRecord(config, ['endpointFor'], 'registration-mismatch', 'RebuildActorRegistrationsConfig');
463
+ const endpointFor = configRecord.get('endpointFor');
464
+ if (typeof endpointFor !== 'function') {
368
465
  throw new ActorAddressingError('registration-mismatch', 'endpointFor must be a function');
369
466
  }
370
467
  const state = runtimeState(runtime);
371
- if (state.registrations.size !== 0) {
372
- throw new ActorAddressingError('already-registered', 'Registration recovery requires a fresh empty local runtime');
373
- }
374
- const endpoints = snapshot.registrations.map((registration) => {
375
- const endpoint = parseEndpoint(config.endpointFor(registration));
376
- if (endpoint.handlerIdentity !== registration.handlerIdentity) {
377
- throw new ActorAddressingError('registration-mismatch', `Recovered endpoint identity does not match snapshot for ${actorAddressKey(registration.address)}`);
378
- }
379
- return endpoint;
380
- });
381
- let nextSequence = state.sequence;
382
- const plannedIds = new Set();
383
- const planned = snapshot.registrations.map((registration, index) => {
384
- nextSequence += 1;
385
- const registrationId = parseOpaqueToken(state.registrationIdFactory({
386
- runtimeInstanceId: runtime.runtimeInstanceId,
387
- sequence: nextSequence,
388
- address: registration.address,
389
- }), 'registrationId', 'registration-mismatch');
390
- if (state.registrationIds.has(registrationId) || plannedIds.has(registrationId)) {
391
- throw new ActorAddressingError('registration-mismatch', `registrationIdFactory returned a duplicate id: ${registrationId}`);
468
+ const operation = 'rebuildActorRegistrations';
469
+ const startingVersion = beginMutation(state, operation);
470
+ try {
471
+ if (state.registrations.size !== 0) {
472
+ throw new ActorAddressingError('already-registered', 'Registration recovery requires a fresh empty local runtime');
392
473
  }
393
- plannedIds.add(registrationId);
394
- const receipt = Object.freeze({
395
- schemaVersion: exports.ACTOR_REGISTRATION_SCHEMA_VERSION,
396
- address: registration.address,
397
- ownerRevision: snapshot.ownerRevision,
398
- snapshotReceipt: snapshot.snapshotReceipt,
399
- handlerIdentity: registration.handlerIdentity,
400
- registrationId,
474
+ const endpoints = snapshot.registrations.map((registration) => {
475
+ const endpoint = parseEndpoint(endpointFor(registration));
476
+ if (endpoint.handlerIdentity !== registration.handlerIdentity) {
477
+ throw new ActorAddressingError('registration-mismatch', `Recovered endpoint identity does not match snapshot for ${actorAddressKey(registration.address)}`);
478
+ }
479
+ return endpoint;
401
480
  });
402
- return {
403
- key: actorAddressKey(registration.address),
404
- registration: {
481
+ if (state.mutationVersion !== startingVersion || state.registrations.size !== 0) {
482
+ throw new ActorAddressingError('registration-mismatch', 'Runtime changed while recovery endpoints were being constructed');
483
+ }
484
+ let nextSequence = state.sequence;
485
+ const plannedIds = new Set();
486
+ const planned = snapshot.registrations.map((registration, index) => {
487
+ nextSequence += 1;
488
+ const registrationId = mintRegistrationId(runtime, state, nextSequence, registration.address, plannedIds);
489
+ plannedIds.add(registrationId);
490
+ const receipt = Object.freeze({
491
+ schemaVersion: exports.ACTOR_REGISTRATION_SCHEMA_VERSION,
405
492
  address: registration.address,
406
- receipt,
407
- endpoint: endpoints[index],
408
- },
409
- };
410
- });
411
- for (const item of planned) {
412
- state.registrations.set(item.key, item.registration);
413
- state.registrationIds.add(item.registration.receipt.registrationId);
493
+ ownerRevision: snapshot.ownerRevision,
494
+ snapshotReceipt: snapshot.snapshotReceipt,
495
+ handlerIdentity: registration.handlerIdentity,
496
+ registrationId,
497
+ });
498
+ return {
499
+ key: actorAddressKey(registration.address),
500
+ registration: {
501
+ address: registration.address,
502
+ receipt,
503
+ endpoint: endpoints[index],
504
+ },
505
+ };
506
+ });
507
+ if (state.mutationVersion !== startingVersion || state.registrations.size !== 0) {
508
+ throw new ActorAddressingError('registration-mismatch', 'Runtime changed while recovery was being planned');
509
+ }
510
+ const nextRegistrations = new Map(planned.map((item) => [item.key, item.registration]));
511
+ const nextUsedRegistrationIds = new Set(state.usedRegistrationIds);
512
+ for (const registrationId of plannedIds) {
513
+ nextUsedRegistrationIds.add(registrationId);
514
+ }
515
+ const receipts = Object.freeze(planned.map((item) => item.registration.receipt));
516
+ state.registrations = nextRegistrations;
517
+ state.usedRegistrationIds = nextUsedRegistrationIds;
518
+ state.sequence = nextSequence;
519
+ state.mutationVersion = startingVersion + 1;
520
+ return receipts;
521
+ }
522
+ finally {
523
+ endMutation(state, operation);
414
524
  }
415
- state.sequence = nextSequence;
416
- return Object.freeze(planned.map((item) => item.registration.receipt));
417
525
  }
418
526
  function createActorRefEndpoint(ref, tag, handlerIdentity) {
419
527
  const parsedHandlerIdentity = parseOpaqueToken(handlerIdentity, 'handlerIdentity', 'registration-mismatch');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "depa-actor",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "main": "./dist-cjs/index.cjs",
6
6
  "module": "./dist/index.js",