solid-objects 0.16.1 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +11 -2
  3. package/dist/actor-runtime.d.ts +7 -0
  4. package/dist/actor-runtime.d.ts.map +1 -1
  5. package/dist/actor.d.ts +1 -0
  6. package/dist/actor.d.ts.map +1 -1
  7. package/dist/actor.js +3 -0
  8. package/dist/actor.js.map +1 -1
  9. package/dist/cloudflare/engine.d.ts +1 -0
  10. package/dist/cloudflare/engine.d.ts.map +1 -1
  11. package/dist/cloudflare/engine.js +118 -15
  12. package/dist/cloudflare/engine.js.map +1 -1
  13. package/dist/cloudflare/records.d.ts +2 -0
  14. package/dist/cloudflare/records.d.ts.map +1 -1
  15. package/dist/cloudflare/runtime.d.ts +7 -0
  16. package/dist/cloudflare/runtime.d.ts.map +1 -1
  17. package/dist/cloudflare/runtime.js +13 -0
  18. package/dist/cloudflare/runtime.js.map +1 -1
  19. package/dist/configuration.d.ts +10 -0
  20. package/dist/configuration.d.ts.map +1 -1
  21. package/dist/configuration.js.map +1 -1
  22. package/dist/diagnostics.d.ts +37 -0
  23. package/dist/diagnostics.d.ts.map +1 -0
  24. package/dist/diagnostics.js +73 -0
  25. package/dist/diagnostics.js.map +1 -0
  26. package/dist/doctor.js +1 -1
  27. package/dist/doctor.js.map +1 -1
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/realtime.d.ts.map +1 -1
  31. package/dist/realtime.js +14 -1
  32. package/dist/realtime.js.map +1 -1
  33. package/dist/reference.d.ts +11 -0
  34. package/dist/reference.d.ts.map +1 -1
  35. package/dist/reference.js +22 -0
  36. package/dist/reference.js.map +1 -1
  37. package/dist/repository.d.ts +1 -1
  38. package/dist/repository.d.ts.map +1 -1
  39. package/dist/repository.js +7 -6
  40. package/dist/repository.js.map +1 -1
  41. package/dist/runtime.d.ts +8 -0
  42. package/dist/runtime.d.ts.map +1 -1
  43. package/dist/runtime.js +108 -32
  44. package/dist/runtime.js.map +1 -1
  45. package/dist/schema.d.ts.map +1 -1
  46. package/dist/schema.js +20 -5
  47. package/dist/schema.js.map +1 -1
  48. package/dist/serialization.js +3 -5
  49. package/dist/serialization.js.map +1 -1
  50. package/dist/telemetry.d.ts +22 -0
  51. package/dist/telemetry.d.ts.map +1 -0
  52. package/dist/telemetry.js +126 -0
  53. package/dist/telemetry.js.map +1 -0
  54. package/dist/transmit.js +2 -2
  55. package/dist/transmit.js.map +1 -1
  56. package/dist/turn.js +2 -2
  57. package/dist/turn.js.map +1 -1
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. package/dist/wake-up-notification.d.ts.map +1 -1
  61. package/dist/wake-up-notification.js +9 -1
  62. package/dist/wake-up-notification.js.map +1 -1
  63. package/docs/agents.md +300 -0
  64. package/docs/api.md +25 -3
  65. package/docs/authorization.md +6 -0
  66. package/docs/comparisons.md +36 -0
  67. package/docs/observability.md +192 -0
  68. package/docs/parity.md +85 -72
  69. package/docs/releasing.md +12 -2
  70. package/docs/virtual-actors.md +242 -0
  71. package/examples/ticket-sale.ts +57 -0
  72. package/package.json +8 -5
package/dist/runtime.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { actorDiagnostics } from "./diagnostics.js";
2
+ import { telemetryEvent, deliverTelemetry } from "./telemetry.js";
1
3
  import { BroadcastWorker } from "./broadcast-worker.js";
2
4
  import { buildSettings, broadcastsEnabled, validateComponent, } from "./configuration.js";
3
5
  import { currentActor, withActorContext, withActorProjection, withApplicationWritesForbidden, } from "./context.js";
@@ -60,6 +62,7 @@ export class SolidObjectsRuntime {
60
62
  realtime;
61
63
  processes;
62
64
  administration;
65
+ observers = new Set();
63
66
  registry = new Map();
64
67
  effects = new Map();
65
68
  commitActions = new Map();
@@ -580,7 +583,7 @@ export class SolidObjectsRuntime {
580
583
  : (await this.repository.remindersForInstance(instance.id)).map(scheduledReminderOf),
581
584
  });
582
585
  const stateBefore = stableJson(actorState(actor, registered.definition.stateKeys));
583
- const intentCount = actor.intentCount();
586
+ const intentsBefore = actor.intentSnapshot();
584
587
  const snapshot = { ...state };
585
588
  await withActorProjection({ actor, runtime: this }, async () => {
586
589
  for (const query of registered.definition.queries) {
@@ -593,9 +596,15 @@ export class SolidObjectsRuntime {
593
596
  }
594
597
  });
595
598
  if (stableJson(actorState(actor, registered.definition.stateKeys)) !== stateBefore ||
596
- actor.intentCount() !== intentCount) {
599
+ actor.intentSnapshot() !== intentsBefore) {
597
600
  throw new QueryMutatedState("snapshot getters must not mutate actor state or stage work");
598
601
  }
602
+ this.emitInstrumentation("snapshot.read", {
603
+ actorType: reference.actorType,
604
+ actorId: reference.actorId,
605
+ instanceId: instance?.id ?? null,
606
+ revision: String(instance?.state_revision ?? 0),
607
+ });
599
608
  return {
600
609
  snapshot: readonlyCopy(snapshot),
601
610
  instanceId: instance?.id ?? "0",
@@ -955,6 +964,10 @@ export class SolidObjectsRuntime {
955
964
  });
956
965
  }
957
966
  catch (error) {
967
+ this.emitInstrumentation("activation.failed", {
968
+ ...activationInstrumentation(turn),
969
+ errorName: error instanceof Error ? error.name : "Error",
970
+ });
958
971
  throw new ActorSetupFailed(error);
959
972
  }
960
973
  const definition = registered.definition;
@@ -968,23 +981,25 @@ export class SolidObjectsRuntime {
968
981
  let stateBefore;
969
982
  if (!activated) {
970
983
  try {
984
+ this.emitInstrumentation("activation.started", activationInstrumentation(turn));
971
985
  await withActorContext({ actor, runtime: this }, () => actor.activate());
972
986
  activated = true;
973
- this.emitInstrumentation("activation.started", {
974
- actorType: turn.message.actor_type,
975
- actorId: turn.message.actor_id,
976
- instanceId: turn.instance.id,
977
- generation: String(turn.activationGeneration),
978
- });
987
+ this.emitInstrumentation("activation.completed", activationInstrumentation(turn));
979
988
  }
980
989
  catch (error) {
981
990
  renewalController.abort();
982
991
  await renewal;
983
992
  actor.discardIntents();
993
+ this.emitInstrumentation("activation.failed", {
994
+ ...activationInstrumentation(turn),
995
+ errorName: error instanceof Error ? error.name : "Error",
996
+ });
984
997
  throw new ActorSetupFailed(renewalError ?? error);
985
998
  }
986
999
  }
987
- const startedAt = Date.now();
1000
+ const startedAt = performance.now();
1001
+ if (Number(turn.message.attempt_count) > 1 && turn.message.error === null)
1002
+ this.emitInstrumentation("recovery.reclaimed", messageInstrumentation(turn.message));
988
1003
  this.emitInstrumentation("message.started", messageInstrumentation(turn.message));
989
1004
  try {
990
1005
  stateBefore = deepCopy(actorState(actor, definition.stateKeys));
@@ -1022,6 +1037,7 @@ export class SolidObjectsRuntime {
1022
1037
  throw new UnknownCommitAction(`unknown commit action ${JSON.stringify(intent.name)}`);
1023
1038
  const attributes = {
1024
1039
  commitAction: intent.name,
1040
+ activationGeneration: String(turn.activationGeneration),
1025
1041
  ...messageInstrumentation(turn.message),
1026
1042
  };
1027
1043
  this.emitInstrumentation("commit_action.started", attributes);
@@ -1064,10 +1080,13 @@ export class SolidObjectsRuntime {
1064
1080
  nextRunAt: new Date(replacement.nextRunAtMilliseconds).toISOString(),
1065
1081
  });
1066
1082
  }
1083
+ if (recoveryMessage(turn.message))
1084
+ this.emitInstrumentation("recovery.completed", messageInstrumentation(turn.message));
1067
1085
  this.warnAboutLargeState(turn.message, committed);
1068
1086
  this.emitInstrumentation("message.completed", {
1069
1087
  ...messageInstrumentation(turn.message),
1070
- durationMilliseconds: Date.now() - startedAt,
1088
+ revision: String(turn.message.sequence),
1089
+ durationMilliseconds: performance.now() - startedAt,
1071
1090
  });
1072
1091
  return { actor, retainActivation: true, activated };
1073
1092
  }
@@ -1080,7 +1099,7 @@ export class SolidObjectsRuntime {
1080
1099
  if (error instanceof LostActivation) {
1081
1100
  this.emitInstrumentation("activation.lost", {
1082
1101
  ...messageInstrumentation(turn.message),
1083
- durationMilliseconds: Date.now() - startedAt,
1102
+ durationMilliseconds: performance.now() - startedAt,
1084
1103
  });
1085
1104
  return { actor, retainActivation: false, activated };
1086
1105
  }
@@ -1093,7 +1112,7 @@ export class SolidObjectsRuntime {
1093
1112
  this.emitInstrumentation("message.rejected", {
1094
1113
  ...messageInstrumentation(turn.message),
1095
1114
  code: error.code,
1096
- durationMilliseconds: Date.now() - startedAt,
1115
+ durationMilliseconds: performance.now() - startedAt,
1097
1116
  });
1098
1117
  return { actor, retainActivation: activated, activated };
1099
1118
  }
@@ -1106,12 +1125,15 @@ export class SolidObjectsRuntime {
1106
1125
  ...messageInstrumentation(turn.message),
1107
1126
  retryable,
1108
1127
  errorName: error instanceof Error ? error.name : "Error",
1109
- durationMilliseconds: Date.now() - startedAt,
1128
+ durationMilliseconds: performance.now() - startedAt,
1110
1129
  outcome,
1111
1130
  });
1112
- if (outcome === "dead") {
1131
+ if (outcome === "retrying")
1132
+ this.emitInstrumentation("message.retry", messageInstrumentation(turn.message));
1133
+ if (outcome === "dead" && recoveryMessage(turn.message))
1134
+ this.emitInstrumentation("recovery.failed", messageInstrumentation(turn.message));
1135
+ if (outcome === "dead")
1113
1136
  this.emitInstrumentation("dead_letter.created", messageInstrumentation(turn.message));
1114
- }
1115
1137
  return { actor, retainActivation: activated, activated };
1116
1138
  }
1117
1139
  }
@@ -1153,6 +1175,7 @@ export class SolidObjectsRuntime {
1153
1175
  await this.callerWorker?.stop();
1154
1176
  this.realtime.close();
1155
1177
  await this.closeWakeUp();
1178
+ this.observers.clear();
1156
1179
  await this.settings.database.close();
1157
1180
  clearDefaultRuntime(this);
1158
1181
  }
@@ -1208,27 +1231,55 @@ export class SolidObjectsRuntime {
1208
1231
  await this.discardWakeUp();
1209
1232
  await this.repository.resetForTesting();
1210
1233
  }
1234
+ diagnostics(reference, options = {}) {
1235
+ return actorDiagnostics({
1236
+ runtime: this,
1237
+ actorType: reference.actorType,
1238
+ actorId: reference.actorId,
1239
+ options,
1240
+ });
1241
+ }
1242
+ async observe(reference, options) {
1243
+ await this.authorizeAdministration({
1244
+ action: "observe",
1245
+ resource: "actor_diagnostics",
1246
+ resourceId: JSON.stringify([reference.actorType, reference.actorId]),
1247
+ authorizationContext: options.authorizationContext,
1248
+ });
1249
+ if (this.observers.size >= 1000)
1250
+ throw new RangeError("at most 1000 local observers may be registered");
1251
+ const observer = (event) => {
1252
+ if (event.actorType === reference.actorType && event.actorId === reference.actorId)
1253
+ return options.onEvent(event);
1254
+ };
1255
+ this.observers.add(observer);
1256
+ return () => {
1257
+ this.observers.delete(observer);
1258
+ };
1259
+ }
1211
1260
  emitInstrumentation(name, attributes) {
1212
- const instrumentation = this.settings.instrumentation;
1213
- if (!instrumentation)
1261
+ if (!this.settings.instrumentation && this.observers.size === 0)
1214
1262
  return;
1215
1263
  try {
1216
- instrumentation(Object.freeze({
1217
- name: `solid_objects.${name}`,
1218
- occurredAt: new Date().toISOString(),
1219
- attributes: readonlyCopy(attributes),
1220
- }));
1221
- }
1222
- catch (error) {
1223
- this.settings.logger.error({
1224
- event: "solid_objects.instrumentation.failed",
1225
- instrumentationEvent: `solid_objects.${name}`,
1226
- errorName: error instanceof Error ? error.name : "Error",
1227
- });
1264
+ const event = telemetryEvent({ name, adapter: this.settings.database.family, attributes });
1265
+ if (this.settings.instrumentation)
1266
+ deliverTelemetry({
1267
+ observer: this.settings.instrumentation,
1268
+ event,
1269
+ logger: this.settings.logger,
1270
+ });
1271
+ for (const observer of this.observers)
1272
+ deliverTelemetry({ observer, event, logger: this.settings.logger });
1228
1273
  }
1274
+ catch { }
1229
1275
  }
1230
1276
  async executeEffect(effect) {
1231
1277
  try {
1278
+ this.emitInstrumentation("outbox.age", {
1279
+ ...effectInstrumentation(effect),
1280
+ outboxKind: "effect",
1281
+ ageMilliseconds: Math.max(0, Date.now() - Number(effect.available_at_ms)),
1282
+ });
1232
1283
  const handler = this.effects.get(effect.name);
1233
1284
  if (!handler)
1234
1285
  throw new UnknownEffect(`unknown effect ${JSON.stringify(effect.name)}`);
@@ -1266,20 +1317,30 @@ export class SolidObjectsRuntime {
1266
1317
  if (!actor.operations.has(dispatchOperation)) {
1267
1318
  throw new UnknownOperation(`unknown reminder operation ${JSON.stringify(dispatchOperation)}`);
1268
1319
  }
1269
- if (!(await this.repository.enqueueReminder(reminder, options)))
1320
+ const message = await this.repository.enqueueReminder(reminder, options);
1321
+ if (!message)
1270
1322
  return;
1271
1323
  this.wakeUp("actors");
1272
1324
  this.emitInstrumentation("reminder.enqueued", {
1273
1325
  reminderId: reminder.id,
1326
+ messageId: message.id,
1327
+ attempt: Number(message.attempt_count),
1274
1328
  actorType: reminder.actor_type,
1275
1329
  actorId: reminder.actor_id,
1276
1330
  operation: reminder.operation,
1331
+ instanceId: reminder.instance_id,
1332
+ latenessMilliseconds: Math.max(0, (options.nowMilliseconds ?? Date.now()) - Number(reminder.run_at_ms)),
1277
1333
  occurrence: Number(reminder.occurrence),
1278
1334
  });
1279
1335
  }
1280
1336
  async executeBroadcast(broadcast) {
1281
1337
  const deliver = this.settings.broadcast;
1282
1338
  try {
1339
+ this.emitInstrumentation("outbox.age", {
1340
+ ...broadcastInstrumentation(broadcast),
1341
+ outboxKind: "broadcast",
1342
+ ageMilliseconds: Math.max(0, Date.now() - Number(broadcast.available_at_ms)),
1343
+ });
1283
1344
  const event = readonlyCopy({
1284
1345
  actorType: broadcast.actor_type,
1285
1346
  actorId: broadcast.actor_id,
@@ -1722,14 +1783,14 @@ export class SolidObjectsRuntime {
1722
1783
  state: deepCopy(options.snapshot.state),
1723
1784
  });
1724
1785
  const stateBefore = stableJson(actorState(actor, options.registered.definition.stateKeys));
1725
- const intentCount = actor.intentCount();
1786
+ const intentsBefore = actor.intentSnapshot();
1726
1787
  const handler = options.registered.definition.payloads[options.name];
1727
1788
  if (!handler) {
1728
1789
  throw new UnknownPayloadBroadcast(`unknown payload broadcast ${options.name}`);
1729
1790
  }
1730
1791
  const rawPayload = await withActorProjection({ actor, runtime: this }, () => handler(actor, options.authorizationContext));
1731
1792
  if (stableJson(actorState(actor, options.registered.definition.stateKeys)) !== stateBefore ||
1732
- actor.intentCount() !== intentCount) {
1793
+ actor.intentSnapshot() !== intentsBefore) {
1733
1794
  throw new QueryMutatedState("payload broadcasts must not mutate actor state or stage work");
1734
1795
  }
1735
1796
  const payload = normalizeJson(rawPayload, { maxBytes: this.settings.maxPayloadBytes });
@@ -1926,8 +1987,14 @@ function messageFailure(message) {
1926
1987
  function actorDestroyedWhileWaiting() {
1927
1988
  return new ActorDestroyed("actor was destroyed while waiting for its result");
1928
1989
  }
1990
+ function recoveryMessage(message) {
1991
+ return (message.delivery_mode === "internal" &&
1992
+ message.idempotency_key?.startsWith("effect:") === true &&
1993
+ message.idempotency_key.endsWith(":recovery"));
1994
+ }
1929
1995
  function messageInstrumentation(message) {
1930
1996
  return {
1997
+ instanceId: message.instance_id,
1931
1998
  messageId: message.id,
1932
1999
  requestId: message.request_id,
1933
2000
  actorType: message.actor_type,
@@ -1938,6 +2005,13 @@ function messageInstrumentation(message) {
1938
2005
  attempt: Number(message.attempt_count),
1939
2006
  };
1940
2007
  }
2008
+ function activationInstrumentation(turn) {
2009
+ return {
2010
+ ...messageInstrumentation(turn.message),
2011
+ generation: String(turn.activationGeneration),
2012
+ ownerId: turn.processId,
2013
+ };
2014
+ }
1941
2015
  function actorMessageContext(message) {
1942
2016
  return Object.freeze({
1943
2017
  id: message.id,
@@ -1957,6 +2031,7 @@ function restoreActorState(options) {
1957
2031
  }
1958
2032
  function effectInstrumentation(effect) {
1959
2033
  return {
2034
+ instanceId: effect.instance_id,
1960
2035
  effectId: effect.id,
1961
2036
  effectName: effect.name,
1962
2037
  messageId: effect.message_id,
@@ -1967,6 +2042,7 @@ function effectInstrumentation(effect) {
1967
2042
  }
1968
2043
  function broadcastInstrumentation(broadcast) {
1969
2044
  return {
2045
+ instanceId: broadcast.instance_id,
1970
2046
  broadcastId: broadcast.id,
1971
2047
  messageId: broadcast.message_id,
1972
2048
  actorType: broadcast.actor_type,