@cadenza.io/service 2.8.0 → 2.10.0

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/index.mjs CHANGED
@@ -2426,6 +2426,8 @@ var RestController = class _RestController {
2426
2426
  constructor() {
2427
2427
  this.fetchClientDiagnostics = /* @__PURE__ */ new Map();
2428
2428
  this.diagnosticsErrorHistoryLimit = 100;
2429
+ this.diagnosticsMaxClientEntries = 500;
2430
+ this.destroyedDiagnosticsTtlMs = 15 * 6e4;
2429
2431
  /**
2430
2432
  * Fetches data from the given URL with a specified timeout. This function performs
2431
2433
  * a fetch request with the ability to cancel the request if it exceeds the provided timeout duration.
@@ -3079,6 +3081,28 @@ var RestController = class _RestController {
3079
3081
  if (!this._instance) this._instance = new _RestController();
3080
3082
  return this._instance;
3081
3083
  }
3084
+ pruneFetchClientDiagnostics(now = Date.now()) {
3085
+ for (const [fetchId, state] of this.fetchClientDiagnostics.entries()) {
3086
+ if (state.destroyed && now - state.updatedAt > this.destroyedDiagnosticsTtlMs) {
3087
+ this.fetchClientDiagnostics.delete(fetchId);
3088
+ }
3089
+ }
3090
+ if (this.fetchClientDiagnostics.size <= this.diagnosticsMaxClientEntries) {
3091
+ return;
3092
+ }
3093
+ const entriesByEvictionPriority = Array.from(
3094
+ this.fetchClientDiagnostics.entries()
3095
+ ).sort((left, right) => {
3096
+ if (left[1].destroyed !== right[1].destroyed) {
3097
+ return left[1].destroyed ? -1 : 1;
3098
+ }
3099
+ return left[1].updatedAt - right[1].updatedAt;
3100
+ });
3101
+ while (this.fetchClientDiagnostics.size > this.diagnosticsMaxClientEntries && entriesByEvictionPriority.length > 0) {
3102
+ const [fetchId] = entriesByEvictionPriority.shift();
3103
+ this.fetchClientDiagnostics.delete(fetchId);
3104
+ }
3105
+ }
3082
3106
  resolveTransportDiagnosticsOptions(ctx) {
3083
3107
  const detailLevel = ctx.detailLevel === "full" ? "full" : "summary";
3084
3108
  const includeErrorHistory = Boolean(ctx.includeErrorHistory);
@@ -3091,6 +3115,8 @@ var RestController = class _RestController {
3091
3115
  };
3092
3116
  }
3093
3117
  ensureFetchClientDiagnostics(fetchId, serviceName, url) {
3118
+ const now = Date.now();
3119
+ this.pruneFetchClientDiagnostics(now);
3094
3120
  let state = this.fetchClientDiagnostics.get(fetchId);
3095
3121
  if (!state) {
3096
3122
  state = {
@@ -3110,13 +3136,14 @@ var RestController = class _RestController {
3110
3136
  signalFailures: 0,
3111
3137
  statusChecks: 0,
3112
3138
  statusFailures: 0,
3113
- updatedAt: Date.now()
3139
+ updatedAt: now
3114
3140
  };
3115
3141
  this.fetchClientDiagnostics.set(fetchId, state);
3116
3142
  } else {
3117
3143
  state.serviceName = serviceName;
3118
3144
  state.url = url;
3119
3145
  }
3146
+ this.pruneFetchClientDiagnostics(now);
3120
3147
  return state;
3121
3148
  }
3122
3149
  getErrorMessage(error) {
@@ -3148,6 +3175,7 @@ var RestController = class _RestController {
3148
3175
  }
3149
3176
  }
3150
3177
  collectFetchTransportDiagnostics(ctx) {
3178
+ this.pruneFetchClientDiagnostics();
3151
3179
  const { detailLevel, includeErrorHistory, errorHistoryLimit } = this.resolveTransportDiagnosticsOptions(ctx);
3152
3180
  const serviceName = CadenzaService.serviceRegistry.serviceName ?? "UnknownService";
3153
3181
  const states = Array.from(this.fetchClientDiagnostics.values()).sort(
@@ -3264,55 +3292,261 @@ var waitForSocketConnection = async (socket, timeoutMs, createError) => {
3264
3292
 
3265
3293
  // src/network/SocketController.ts
3266
3294
  var SocketController = class _SocketController {
3267
- /**
3268
- * Constructs the `SocketServer`, setting up a WebSocket server with specific configurations,
3269
- * including connection state recovery, rate limiting, CORS handling, and custom event handling.
3270
- * This class sets up the communication infrastructure for scalable, resilient, and secure WebSocket-based interactions
3271
- * using metadata-driven task execution with `Cadenza`.
3272
- *
3273
- * It provides support for:
3274
- * - Origin-based access control for connections.
3275
- * - Optional payload sanitization.
3276
- * - Configurable rate limiting and behavior on limit breaches (soft/hard disconnects).
3277
- * - Event handlers for connection, handshake, delegation, signaling, status checks, and disconnection.
3278
- *
3279
- * The server can handle both internal and external interactions depending on the provided configurations,
3280
- * and integrates directly with Cadenza's task workflow engine.
3281
- *
3282
- * Initializes the `SocketServer` to be ready for WebSocket communication.
3283
- */
3284
3295
  constructor() {
3285
- this.socketClientDiagnostics = /* @__PURE__ */ new Map();
3286
3296
  this.diagnosticsErrorHistoryLimit = 100;
3297
+ this.diagnosticsMaxClientEntries = 500;
3298
+ this.destroyedDiagnosticsTtlMs = 15 * 6e4;
3299
+ this.socketServerDefaultKey = "socket-server-default";
3300
+ this.socketServerInitialSessionState = {
3301
+ serverKey: this.socketServerDefaultKey,
3302
+ useSocket: false,
3303
+ status: "inactive",
3304
+ securityProfile: "medium",
3305
+ networkType: "internal",
3306
+ connectionCount: 0,
3307
+ lastStartedAt: null,
3308
+ lastConnectedAt: null,
3309
+ lastDisconnectedAt: null,
3310
+ lastShutdownAt: null,
3311
+ updatedAt: 0
3312
+ };
3313
+ this.socketClientInitialSessionState = {
3314
+ fetchId: "",
3315
+ serviceInstanceId: "",
3316
+ communicationTypes: [],
3317
+ serviceName: "",
3318
+ serviceAddress: "",
3319
+ servicePort: 0,
3320
+ protocol: "http",
3321
+ url: "",
3322
+ socketId: null,
3323
+ connected: false,
3324
+ handshake: false,
3325
+ pendingDelegations: 0,
3326
+ pendingTimers: 0,
3327
+ reconnectAttempts: 0,
3328
+ connectErrors: 0,
3329
+ reconnectErrors: 0,
3330
+ socketErrors: 0,
3331
+ errorCount: 0,
3332
+ destroyed: false,
3333
+ lastHandshakeAt: null,
3334
+ lastHandshakeError: null,
3335
+ lastDisconnectAt: null,
3336
+ updatedAt: 0
3337
+ };
3338
+ this.socketServerActor = CadenzaService.createActor(
3339
+ {
3340
+ name: "SocketServerActor",
3341
+ description: "Holds durable socket server session state and runtime socket server handle",
3342
+ defaultKey: this.socketServerDefaultKey,
3343
+ keyResolver: (input) => this.resolveSocketServerKey(input),
3344
+ loadPolicy: "lazy",
3345
+ writeContract: "overwrite",
3346
+ initState: this.socketServerInitialSessionState
3347
+ },
3348
+ { isMeta: true }
3349
+ );
3350
+ this.socketClientActor = CadenzaService.createActor(
3351
+ {
3352
+ name: "SocketClientActor",
3353
+ description: "Holds durable socket client session state and runtime socket connection handles",
3354
+ defaultKey: "socket-client-default",
3355
+ keyResolver: (input) => this.resolveSocketClientFetchId(input),
3356
+ loadPolicy: "lazy",
3357
+ writeContract: "overwrite",
3358
+ initState: this.socketClientInitialSessionState
3359
+ },
3360
+ { isMeta: true }
3361
+ );
3362
+ this.socketClientDiagnosticsActor = CadenzaService.createActor(
3363
+ {
3364
+ name: "SocketClientDiagnosticsActor",
3365
+ description: "Tracks socket client diagnostics snapshots per fetchId for transport observability",
3366
+ defaultKey: "socket-client-diagnostics",
3367
+ loadPolicy: "eager",
3368
+ writeContract: "overwrite",
3369
+ initState: {
3370
+ entries: {}
3371
+ }
3372
+ },
3373
+ { isMeta: true }
3374
+ );
3375
+ this.registerDiagnosticsTasks();
3376
+ this.registerSocketServerTasks();
3377
+ this.registerSocketClientTasks();
3287
3378
  CadenzaService.createMetaTask(
3288
3379
  "Collect socket transport diagnostics",
3289
- (ctx) => this.collectSocketTransportDiagnostics(ctx),
3380
+ this.socketClientDiagnosticsActor.task(
3381
+ ({ state, input }) => this.collectSocketTransportDiagnostics(input, state.entries),
3382
+ { mode: "read" }
3383
+ ),
3290
3384
  "Responds to distributed transport diagnostics inquiries with socket client data."
3291
3385
  ).respondsTo(META_RUNTIME_TRANSPORT_DIAGNOSTICS_INTENT);
3292
- CadenzaService.createMetaRoutine(
3293
- "SocketServer",
3294
- [
3295
- CadenzaService.createMetaTask("Setup SocketServer", (ctx) => {
3296
- if (!ctx.__useSocket) {
3386
+ }
3387
+ static get instance() {
3388
+ if (!this._instance) this._instance = new _SocketController();
3389
+ return this._instance;
3390
+ }
3391
+ registerDiagnosticsTasks() {
3392
+ CadenzaService.createThrottledMetaTask(
3393
+ "SocketClientDiagnosticsActor.Upsert",
3394
+ this.socketClientDiagnosticsActor.task(
3395
+ ({ state, input, setState }) => {
3396
+ const fetchId = String(input.fetchId ?? "").trim();
3397
+ if (!fetchId) {
3297
3398
  return;
3298
3399
  }
3299
- const server = new Server(ctx.httpsServer ?? ctx.httpServer, {
3300
- pingInterval: 3e4,
3301
- pingTimeout: 2e4,
3302
- maxHttpBufferSize: 1e7,
3303
- // 10MB large payloads
3304
- connectionStateRecovery: {
3305
- maxDisconnectionDuration: 2 * 60 * 1e3,
3306
- // 2min
3307
- skipMiddlewares: true
3308
- // Optional: bypass rate limiter on recover
3400
+ const now = Date.now();
3401
+ const entries = { ...state.entries };
3402
+ const existing = entries[fetchId];
3403
+ const base = existing ? {
3404
+ ...existing,
3405
+ errorHistory: [...existing.errorHistory]
3406
+ } : {
3407
+ fetchId,
3408
+ serviceName: String(input.serviceName ?? ""),
3409
+ url: String(input.url ?? ""),
3410
+ socketId: null,
3411
+ connected: false,
3412
+ handshake: false,
3413
+ reconnectAttempts: 0,
3414
+ connectErrors: 0,
3415
+ reconnectErrors: 0,
3416
+ socketErrors: 0,
3417
+ pendingDelegations: 0,
3418
+ pendingTimers: 0,
3419
+ destroyed: false,
3420
+ lastHandshakeAt: null,
3421
+ lastHandshakeError: null,
3422
+ lastDisconnectAt: null,
3423
+ lastError: null,
3424
+ lastErrorAt: 0,
3425
+ errorHistory: [],
3426
+ updatedAt: now
3427
+ };
3428
+ if (input.serviceName !== void 0) {
3429
+ base.serviceName = String(input.serviceName);
3430
+ }
3431
+ if (input.url !== void 0) {
3432
+ base.url = String(input.url);
3433
+ }
3434
+ const patch = input.patch && typeof input.patch === "object" ? input.patch : {};
3435
+ Object.assign(base, patch);
3436
+ base.fetchId = fetchId;
3437
+ base.updatedAt = now;
3438
+ const errorMessage = input.error !== void 0 ? this.getErrorMessage(input.error) : void 0;
3439
+ if (errorMessage) {
3440
+ base.lastError = errorMessage;
3441
+ base.lastErrorAt = now;
3442
+ base.errorHistory.push({
3443
+ at: new Date(now).toISOString(),
3444
+ message: errorMessage
3445
+ });
3446
+ if (base.errorHistory.length > this.diagnosticsErrorHistoryLimit) {
3447
+ base.errorHistory.splice(
3448
+ 0,
3449
+ base.errorHistory.length - this.diagnosticsErrorHistoryLimit
3450
+ );
3309
3451
  }
3452
+ }
3453
+ entries[fetchId] = base;
3454
+ this.pruneDiagnosticsEntries(entries, now);
3455
+ setState({ entries });
3456
+ },
3457
+ { mode: "write" }
3458
+ ),
3459
+ (context) => String(context?.fetchId ?? "default"),
3460
+ "Upserts socket client diagnostics in actor state."
3461
+ ).doOn("meta.socket_client.diagnostics_upsert_requested");
3462
+ }
3463
+ registerSocketServerTasks() {
3464
+ CadenzaService.createThrottledMetaTask(
3465
+ "SocketServerActor.PatchSession",
3466
+ this.socketServerActor.task(
3467
+ ({ state, input, setState }) => {
3468
+ const patch = input.patch && typeof input.patch === "object" ? input.patch : {};
3469
+ setState({
3470
+ ...state,
3471
+ ...patch,
3472
+ updatedAt: Date.now()
3473
+ });
3474
+ },
3475
+ { mode: "write" }
3476
+ ),
3477
+ (context) => String(context?.serverKey ?? this.socketServerDefaultKey),
3478
+ "Applies partial durable session updates for socket server actor."
3479
+ ).doOn("meta.socket_server.session_patch_requested");
3480
+ CadenzaService.createMetaTask(
3481
+ "SocketServerActor.ClearRuntime",
3482
+ this.socketServerActor.task(
3483
+ ({ setRuntimeState }) => {
3484
+ setRuntimeState(null);
3485
+ },
3486
+ { mode: "write" }
3487
+ ),
3488
+ "Clears socket server runtime handle after shutdown."
3489
+ ).doOn("meta.socket_server.runtime_clear_requested");
3490
+ const setupSocketServerTask = CadenzaService.createMetaTask(
3491
+ "Setup SocketServer",
3492
+ this.socketServerActor.task(
3493
+ ({ state, runtimeState, input, actor, setState, setRuntimeState, emit }) => {
3494
+ const serverKey = this.resolveSocketServerKey(input) ?? actor.key ?? this.socketServerDefaultKey;
3495
+ const shouldUseSocket = Boolean(input.__useSocket);
3496
+ if (!shouldUseSocket) {
3497
+ this.destroySocketServerRuntimeHandle(runtimeState);
3498
+ setRuntimeState(null);
3499
+ setState({
3500
+ ...state,
3501
+ serverKey,
3502
+ useSocket: false,
3503
+ status: "inactive",
3504
+ connectionCount: 0,
3505
+ lastShutdownAt: (/* @__PURE__ */ new Date()).toISOString(),
3506
+ updatedAt: Date.now()
3507
+ });
3508
+ return;
3509
+ }
3510
+ let runtimeHandle = runtimeState;
3511
+ if (!runtimeHandle) {
3512
+ runtimeHandle = this.createSocketServerRuntimeHandleFromContext(input);
3513
+ setRuntimeState(runtimeHandle);
3514
+ }
3515
+ const profile = String(input.__securityProfile ?? state.securityProfile ?? "medium");
3516
+ const networkType = String(input.__networkType ?? state.networkType ?? "internal");
3517
+ const schedulePatch = (patch) => {
3518
+ CadenzaService.emit("meta.socket_server.session_patch_requested", {
3519
+ serverKey,
3520
+ patch
3521
+ });
3522
+ };
3523
+ if (runtimeHandle.initialized) {
3524
+ schedulePatch({
3525
+ status: "active",
3526
+ useSocket: true,
3527
+ securityProfile: profile,
3528
+ networkType,
3529
+ connectionCount: runtimeHandle.connectedSocketIds.size,
3530
+ lastStartedAt: state.lastStartedAt ?? (/* @__PURE__ */ new Date()).toISOString()
3531
+ });
3532
+ return;
3533
+ }
3534
+ const server = runtimeHandle.server;
3535
+ runtimeHandle.initialized = true;
3536
+ setState({
3537
+ ...state,
3538
+ serverKey,
3539
+ useSocket: true,
3540
+ status: "active",
3541
+ securityProfile: profile,
3542
+ networkType,
3543
+ connectionCount: runtimeHandle.connectedSocketIds.size,
3544
+ lastStartedAt: state.lastStartedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
3545
+ updatedAt: Date.now()
3310
3546
  });
3311
- const profile = ctx.__securityProfile ?? "medium";
3312
3547
  server.use((socket, next) => {
3313
3548
  const origin = socket?.handshake?.headers?.origin;
3314
3549
  const allowedOrigins = ["*"];
3315
- const networkType = ctx.__networkType ?? "internal";
3316
3550
  let effectiveOrigin = origin || "unknown";
3317
3551
  if (networkType === "internal") effectiveOrigin = "internal";
3318
3552
  if (profile !== "low" && !allowedOrigins.includes(effectiveOrigin) && !allowedOrigins.includes("*")) {
@@ -3323,7 +3557,9 @@ var SocketController = class _SocketController {
3323
3557
  medium: { points: 1e4, duration: 10 },
3324
3558
  high: { points: 1e3, duration: 60, blockDuration: 300 }
3325
3559
  };
3326
- const limiter = new RateLimiterMemory2(limiterOptions[profile]);
3560
+ const limiter = new RateLimiterMemory2(
3561
+ limiterOptions[profile] ?? limiterOptions.medium
3562
+ );
3327
3563
  const clientKey = socket?.handshake?.address || "unknown";
3328
3564
  socket.use((packet, packetNext) => {
3329
3565
  limiter.consume(clientKey).then(() => packetNext()).catch((rej) => {
@@ -3358,116 +3594,111 @@ var SocketController = class _SocketController {
3358
3594
  });
3359
3595
  next();
3360
3596
  });
3361
- if (!server) {
3362
- CadenzaService.log("Socket setup error: No server", {}, "error");
3363
- return { ...ctx, __error: "No server", errored: true };
3364
- }
3365
3597
  server.on("connection", (ws) => {
3598
+ runtimeHandle.connectedSocketIds.add(ws.id);
3599
+ schedulePatch({
3600
+ connectionCount: runtimeHandle.connectedSocketIds.size,
3601
+ lastConnectedAt: (/* @__PURE__ */ new Date()).toISOString(),
3602
+ status: "active"
3603
+ });
3366
3604
  try {
3367
- ws.on(
3368
- "handshake",
3369
- (ctx2, callback) => {
3370
- CadenzaService.log("SocketServer: New connection", {
3371
- ...ctx2,
3372
- socketId: ws.id
3373
- });
3374
- callback({
3375
- status: "success",
3376
- serviceName: CadenzaService.serviceRegistry.serviceName
3377
- });
3378
- if (ctx2.isFrontend) {
3379
- const fetchId = `browser:${ctx2.serviceInstanceId}`;
3380
- CadenzaService.createMetaTask(
3381
- `Transmit signal to ${fetchId}`,
3382
- (ctx3, emit) => {
3383
- if (ctx3.__signalName === void 0) {
3384
- return;
3385
- }
3386
- ws.emit("signal", ctx3);
3387
- if (ctx3.__routineExecId) {
3388
- emit(
3389
- `meta.socket_client.transmitted:${ctx3.__routineExecId}`,
3390
- {}
3391
- );
3392
- }
3605
+ ws.on("handshake", (ctx, callback) => {
3606
+ CadenzaService.log("SocketServer: New connection", {
3607
+ ...ctx,
3608
+ socketId: ws.id
3609
+ });
3610
+ callback({
3611
+ status: "success",
3612
+ serviceName: CadenzaService.serviceRegistry.serviceName
3613
+ });
3614
+ if (ctx.isFrontend) {
3615
+ const fetchId = `browser:${ctx.serviceInstanceId}`;
3616
+ CadenzaService.createMetaTask(
3617
+ `Transmit signal to ${fetchId}`,
3618
+ (c, emitter) => {
3619
+ if (c.__signalName === void 0) {
3620
+ return;
3621
+ }
3622
+ ws.emit("signal", c);
3623
+ if (c.__routineExecId) {
3624
+ emitter(`meta.socket_client.transmitted:${c.__routineExecId}`, {});
3393
3625
  }
3394
- ).doOn(
3395
- `meta.service_registry.selected_instance_for_socket:${fetchId}`
3396
- ).attachSignal("meta.socket_client.transmitted");
3397
- }
3398
- CadenzaService.emit("meta.socket.handshake", ctx2);
3399
- }
3400
- );
3401
- ws.on(
3402
- "delegation",
3403
- (ctx2, callback) => {
3404
- const deputyExecId = ctx2.__metadata.__deputyExecId;
3405
- CadenzaService.createEphemeralMetaTask(
3406
- "Resolve delegation",
3407
- (ctx3) => {
3408
- callback(ctx3);
3409
- },
3410
- "Resolves a delegation request using the provided callback from the client (.emitWithAck())",
3411
- { register: false }
3412
- ).doOn(`meta.node.graph_completed:${deputyExecId}`).emits(`meta.socket.delegation_resolved:${deputyExecId}`);
3413
- CadenzaService.createEphemeralMetaTask(
3414
- "Delegation progress update",
3415
- (ctx3) => {
3416
- if (ctx3.__progress !== void 0)
3417
- ws.emit("delegation_progress", ctx3);
3418
3626
  },
3419
- "Updates delegation progress",
3420
- {
3421
- once: false,
3422
- destroyCondition: (ctx3) => ctx3.data.progress === 1 || ctx3.data?.progress === void 0,
3423
- register: false
3424
- }
3425
- ).doOn(
3426
- `meta.node.routine_execution_progress:${deputyExecId}`,
3427
- `meta.node.graph_completed:${deputyExecId}`
3428
- ).emitsOnFail(`meta.socket.progress_failed:${deputyExecId}`);
3429
- CadenzaService.emit("meta.socket.delegation_requested", {
3430
- ...ctx2,
3431
- __name: ctx2.__remoteRoutineName
3432
- });
3627
+ "Transmit frontend bound signal through active websocket."
3628
+ ).doOn(`meta.service_registry.selected_instance_for_socket:${fetchId}`).attachSignal("meta.socket_client.transmitted");
3433
3629
  }
3434
- );
3435
- ws.on(
3436
- "signal",
3437
- (ctx2, callback) => {
3438
- if (CadenzaService.signalBroker.listObservedSignals().includes(ctx2.__signalName)) {
3439
- callback({
3440
- __status: "success",
3441
- __signalName: ctx2.__signalName
3442
- });
3443
- CadenzaService.emit(ctx2.__signalName, ctx2);
3444
- } else {
3445
- CadenzaService.log(
3446
- `No such signal ${ctx2.__signalName} on ${ctx2.__serviceName}`,
3447
- "warning"
3448
- );
3449
- callback({
3450
- ...ctx2,
3451
- __status: "error",
3452
- __error: `No such signal: ${ctx2.__signalName}`,
3453
- errored: true
3454
- });
3630
+ CadenzaService.emit("meta.socket.handshake", ctx);
3631
+ });
3632
+ ws.on("delegation", (ctx, callback) => {
3633
+ const deputyExecId = ctx.__metadata.__deputyExecId;
3634
+ CadenzaService.createEphemeralMetaTask(
3635
+ "Resolve delegation",
3636
+ (delegationCtx) => {
3637
+ callback(delegationCtx);
3638
+ },
3639
+ "Resolves a delegation request using client callback.",
3640
+ { register: false }
3641
+ ).doOn(`meta.node.graph_completed:${deputyExecId}`).emits(`meta.socket.delegation_resolved:${deputyExecId}`);
3642
+ CadenzaService.createEphemeralMetaTask(
3643
+ "Delegation progress update",
3644
+ (progressCtx) => {
3645
+ if (progressCtx.__progress !== void 0) {
3646
+ ws.emit("delegation_progress", progressCtx);
3647
+ }
3648
+ },
3649
+ "Updates delegation progress to client.",
3650
+ {
3651
+ once: false,
3652
+ destroyCondition: (progressCtx) => progressCtx.data.progress === 1 || progressCtx.data?.progress === void 0,
3653
+ register: false
3455
3654
  }
3655
+ ).doOn(
3656
+ `meta.node.routine_execution_progress:${deputyExecId}`,
3657
+ `meta.node.graph_completed:${deputyExecId}`
3658
+ ).emitsOnFail(`meta.socket.progress_failed:${deputyExecId}`);
3659
+ CadenzaService.emit("meta.socket.delegation_requested", {
3660
+ ...ctx,
3661
+ __name: ctx.__remoteRoutineName
3662
+ });
3663
+ });
3664
+ ws.on("signal", (ctx, callback) => {
3665
+ if (CadenzaService.signalBroker.listObservedSignals().includes(ctx.__signalName)) {
3666
+ callback({
3667
+ __status: "success",
3668
+ __signalName: ctx.__signalName
3669
+ });
3670
+ CadenzaService.emit(ctx.__signalName, ctx);
3671
+ } else {
3672
+ CadenzaService.log(
3673
+ `No such signal ${ctx.__signalName} on ${ctx.__serviceName}`,
3674
+ "warning"
3675
+ );
3676
+ callback({
3677
+ ...ctx,
3678
+ __status: "error",
3679
+ __error: `No such signal: ${ctx.__signalName}`,
3680
+ errored: true
3681
+ });
3456
3682
  }
3457
- );
3683
+ });
3458
3684
  ws.on(
3459
3685
  "status_check",
3460
- (ctx2, callback) => {
3686
+ (ctx, callback) => {
3461
3687
  CadenzaService.createEphemeralMetaTask(
3462
3688
  "Resolve status check",
3463
3689
  callback,
3464
3690
  "Resolves a status check request",
3465
3691
  { register: false }
3466
3692
  ).doAfter(CadenzaService.serviceRegistry.getStatusTask);
3467
- CadenzaService.emit("meta.socket.status_check_requested", ctx2);
3693
+ CadenzaService.emit("meta.socket.status_check_requested", ctx);
3468
3694
  }
3469
3695
  );
3470
3696
  ws.on("disconnect", () => {
3697
+ runtimeHandle.connectedSocketIds.delete(ws.id);
3698
+ schedulePatch({
3699
+ connectionCount: runtimeHandle.connectedSocketIds.size,
3700
+ lastDisconnectedAt: (/* @__PURE__ */ new Date()).toISOString()
3701
+ });
3471
3702
  CadenzaService.log(
3472
3703
  "Socket client disconnected",
3473
3704
  { socketId: ws.id },
@@ -3477,514 +3708,846 @@ var SocketController = class _SocketController {
3477
3708
  __wsId: ws.id
3478
3709
  });
3479
3710
  });
3480
- } catch (e) {
3711
+ } catch (error) {
3481
3712
  CadenzaService.log(
3482
3713
  "SocketServer: Error in socket event",
3483
- { error: e },
3714
+ { error },
3484
3715
  "error"
3485
3716
  );
3486
3717
  }
3487
3718
  CadenzaService.emit("meta.socket.connected", { __wsId: ws.id });
3488
3719
  });
3489
- CadenzaService.createMetaTask(
3490
- "Broadcast status",
3491
- (ctx2) => server.emit("status_update", ctx2),
3720
+ runtimeHandle.broadcastStatusTask = CadenzaService.createMetaTask(
3721
+ `Broadcast status ${serverKey}`,
3722
+ (ctx) => server.emit("status_update", ctx),
3492
3723
  "Broadcasts the status of the server to all clients"
3493
3724
  ).doOn("meta.service.updated");
3494
- CadenzaService.createMetaTask(
3495
- "Shutdown SocketServer",
3496
- () => server.close(),
3725
+ runtimeHandle.shutdownTask = CadenzaService.createMetaTask(
3726
+ `Shutdown SocketServer ${serverKey}`,
3727
+ async () => {
3728
+ this.destroySocketServerRuntimeHandle(runtimeHandle);
3729
+ CadenzaService.emit("meta.socket_server.runtime_clear_requested", {
3730
+ serverKey
3731
+ });
3732
+ CadenzaService.emit("meta.socket_server.session_patch_requested", {
3733
+ serverKey,
3734
+ patch: {
3735
+ useSocket: false,
3736
+ status: "shutdown",
3737
+ connectionCount: 0,
3738
+ lastShutdownAt: (/* @__PURE__ */ new Date()).toISOString()
3739
+ }
3740
+ });
3741
+ },
3497
3742
  "Shuts down the socket server"
3498
3743
  ).doOn("meta.socket_server_shutdown_requested").emits("meta.socket.shutdown");
3499
- return ctx;
3500
- })
3501
- ],
3502
- "Bootstraps the socket server"
3503
- ).doOn("global.meta.rest.network_configured");
3744
+ return true;
3745
+ },
3746
+ { mode: "write" }
3747
+ ),
3748
+ "Initializes socket server runtime through actor state."
3749
+ );
3750
+ setupSocketServerTask.doOn("global.meta.rest.network_configured");
3751
+ }
3752
+ registerSocketClientTasks() {
3753
+ CadenzaService.createThrottledMetaTask(
3754
+ "SocketClientActor.ApplySessionOperation",
3755
+ this.socketClientActor.task(
3756
+ ({ state, input, setState }) => {
3757
+ const operation = String(
3758
+ input.operation ?? "transmit"
3759
+ );
3760
+ const patch = input.patch && typeof input.patch === "object" ? input.patch : {};
3761
+ let next = {
3762
+ ...state,
3763
+ ...patch,
3764
+ communicationTypes: patch.communicationTypes !== void 0 ? this.normalizeCommunicationTypes(patch.communicationTypes) : state.communicationTypes,
3765
+ updatedAt: Date.now()
3766
+ };
3767
+ if (input.serviceName !== void 0) {
3768
+ next.serviceName = String(input.serviceName);
3769
+ }
3770
+ if (input.serviceAddress !== void 0) {
3771
+ next.serviceAddress = String(input.serviceAddress);
3772
+ }
3773
+ if (input.serviceInstanceId !== void 0) {
3774
+ next.serviceInstanceId = String(input.serviceInstanceId);
3775
+ }
3776
+ if (input.protocol !== void 0) {
3777
+ next.protocol = String(input.protocol);
3778
+ }
3779
+ if (input.url !== void 0) {
3780
+ next.url = String(input.url);
3781
+ }
3782
+ if (input.servicePort !== void 0) {
3783
+ next.servicePort = Number(input.servicePort);
3784
+ }
3785
+ if (input.fetchId !== void 0) {
3786
+ next.fetchId = String(input.fetchId);
3787
+ }
3788
+ if (operation === "connect") {
3789
+ next.destroyed = false;
3790
+ } else if (operation === "handshake") {
3791
+ next.destroyed = false;
3792
+ next.connected = patch.connected ?? true;
3793
+ next.handshake = patch.handshake ?? true;
3794
+ } else if (operation === "shutdown") {
3795
+ next.connected = false;
3796
+ next.handshake = false;
3797
+ next.destroyed = true;
3798
+ next.pendingDelegations = 0;
3799
+ next.pendingTimers = 0;
3800
+ }
3801
+ setState(next);
3802
+ return next;
3803
+ },
3804
+ { mode: "write" }
3805
+ ),
3806
+ (context) => String(this.resolveSocketClientFetchId(context ?? {}) ?? "default"),
3807
+ "Applies socket client session operation patch in actor durable state."
3808
+ ).doOn("meta.socket_client.session_operation_requested");
3809
+ CadenzaService.createMetaTask(
3810
+ "SocketClientActor.ClearRuntime",
3811
+ this.socketClientActor.task(
3812
+ ({ setRuntimeState }) => {
3813
+ setRuntimeState(null);
3814
+ },
3815
+ { mode: "write" }
3816
+ ),
3817
+ "Clears socket client runtime handle."
3818
+ ).doOn("meta.socket_client.runtime_clear_requested");
3504
3819
  CadenzaService.createMetaTask(
3505
3820
  "Connect to socket server",
3506
- (ctx) => {
3507
- const {
3508
- serviceInstanceId,
3509
- communicationTypes,
3510
- serviceName,
3511
- serviceAddress,
3512
- servicePort,
3513
- protocol
3514
- } = ctx;
3515
- const socketProtocol = protocol === "https" ? "wss" : "ws";
3516
- const port = protocol === "https" ? 443 : servicePort;
3517
- const URL = `${socketProtocol}://${serviceAddress}:${port}`;
3518
- const fetchId = `${serviceAddress}_${port}`;
3519
- const socketDiagnostics = this.ensureSocketClientDiagnostics(
3520
- fetchId,
3521
- serviceName,
3522
- URL
3523
- );
3524
- socketDiagnostics.destroyed = false;
3525
- socketDiagnostics.updatedAt = Date.now();
3526
- let handshake = false;
3527
- let errorCount = 0;
3528
- const ERROR_LIMIT = 5;
3529
- if (CadenzaService.get(`Socket handshake with ${URL}`)) {
3530
- console.error("Socket client already exists", URL);
3531
- return;
3532
- }
3533
- const pendingDelegationIds = /* @__PURE__ */ new Set();
3534
- const pendingTimers = /* @__PURE__ */ new Set();
3535
- const syncPendingCounts = () => {
3536
- socketDiagnostics.pendingDelegations = pendingDelegationIds.size;
3537
- socketDiagnostics.pendingTimers = pendingTimers.size;
3538
- socketDiagnostics.updatedAt = Date.now();
3539
- };
3540
- let handshakeTask = null;
3541
- let emitWhenReady = null;
3542
- let transmitTask = null;
3543
- let delegateTask = null;
3544
- let socket = null;
3545
- socket = io(URL, {
3546
- reconnection: true,
3547
- reconnectionAttempts: 5,
3548
- reconnectionDelay: 2e3,
3549
- reconnectionDelayMax: 1e4,
3550
- randomizationFactor: 0.5,
3551
- transports: ["websocket"],
3552
- autoConnect: false
3553
- });
3554
- emitWhenReady = (event, data, timeoutMs = 6e4, ack) => {
3555
- return new Promise((resolve) => {
3556
- const resolveWithError = (errorMessage, fallbackError) => {
3557
- resolve({
3558
- ...data,
3559
- errored: true,
3560
- __error: errorMessage,
3561
- error: fallbackError instanceof Error ? fallbackError.message : errorMessage,
3562
- socketId: socket?.id,
3821
+ this.socketClientActor.task(
3822
+ ({ state, runtimeState, input, setState, setRuntimeState, emit }) => {
3823
+ const serviceInstanceId = String(input.serviceInstanceId ?? "");
3824
+ const communicationTypes = this.normalizeCommunicationTypes(
3825
+ input.communicationTypes
3826
+ );
3827
+ const serviceName = String(input.serviceName ?? "");
3828
+ const serviceAddress = String(input.serviceAddress ?? "");
3829
+ const protocol = String(input.protocol ?? "http");
3830
+ const normalizedPort = this.resolveServicePort(protocol, input.servicePort);
3831
+ if (!serviceAddress || !normalizedPort) {
3832
+ CadenzaService.log(
3833
+ "Socket client setup skipped due to missing address/port",
3834
+ {
3563
3835
  serviceName,
3564
- URL
3565
- });
3566
- };
3567
- const tryEmit = async () => {
3568
- const waitTimeoutMs = timeoutMs > 0 ? timeoutMs + 10 : 1e4;
3569
- const waitResult = await waitForSocketConnection(
3570
- socket,
3571
- waitTimeoutMs,
3572
- (reason, error) => {
3573
- if (reason === "connect_timeout") {
3574
- return `Socket connect timed out before '${event}'`;
3575
- }
3576
- if (reason === "connect_error") {
3577
- const errMessage = error instanceof Error ? error.message : String(error);
3578
- return `Socket connect error before '${event}': ${errMessage}`;
3579
- }
3580
- return `Socket disconnected before '${event}'`;
3836
+ serviceAddress,
3837
+ servicePort: input.servicePort,
3838
+ protocol
3839
+ },
3840
+ "warning"
3841
+ );
3842
+ return false;
3843
+ }
3844
+ const socketProtocol = protocol === "https" ? "wss" : "ws";
3845
+ const url = `${socketProtocol}://${serviceAddress}:${normalizedPort}`;
3846
+ const fetchId = `${serviceAddress}_${normalizedPort}`;
3847
+ const applySessionOperation = (operation, patch = {}) => {
3848
+ CadenzaService.emit("meta.socket_client.session_operation_requested", {
3849
+ fetchId,
3850
+ operation,
3851
+ patch,
3852
+ serviceInstanceId,
3853
+ communicationTypes,
3854
+ serviceName,
3855
+ serviceAddress,
3856
+ servicePort: normalizedPort,
3857
+ protocol,
3858
+ url
3859
+ });
3860
+ };
3861
+ const upsertDiagnostics = (patch, error) => {
3862
+ CadenzaService.emit("meta.socket_client.diagnostics_upsert_requested", {
3863
+ fetchId,
3864
+ serviceName,
3865
+ url,
3866
+ patch,
3867
+ error
3868
+ });
3869
+ };
3870
+ setState({
3871
+ ...state,
3872
+ fetchId,
3873
+ serviceInstanceId,
3874
+ communicationTypes,
3875
+ serviceName,
3876
+ serviceAddress,
3877
+ servicePort: normalizedPort,
3878
+ protocol,
3879
+ url,
3880
+ destroyed: false,
3881
+ updatedAt: Date.now()
3882
+ });
3883
+ let runtimeHandle = runtimeState;
3884
+ if (!runtimeHandle || runtimeHandle.url !== url) {
3885
+ this.destroySocketClientRuntimeHandle(runtimeHandle);
3886
+ runtimeHandle = this.createSocketClientRuntimeHandle(url);
3887
+ setRuntimeState(runtimeHandle);
3888
+ }
3889
+ upsertDiagnostics({
3890
+ destroyed: false,
3891
+ connected: false,
3892
+ handshake: false,
3893
+ socketId: runtimeHandle.socket.id ?? null
3894
+ });
3895
+ applySessionOperation("connect", {
3896
+ destroyed: false,
3897
+ connected: false,
3898
+ handshake: false,
3899
+ socketId: runtimeHandle.socket.id ?? null,
3900
+ pendingDelegations: runtimeHandle.pendingDelegationIds.size,
3901
+ pendingTimers: runtimeHandle.pendingTimers.size,
3902
+ errorCount: runtimeHandle.errorCount
3903
+ });
3904
+ if (runtimeHandle.initialized) {
3905
+ return true;
3906
+ }
3907
+ runtimeHandle.initialized = true;
3908
+ runtimeHandle.handshake = false;
3909
+ runtimeHandle.errorCount = 0;
3910
+ const syncPendingCounts = () => {
3911
+ const pendingDelegations = runtimeHandle.pendingDelegationIds.size;
3912
+ const pendingTimers = runtimeHandle.pendingTimers.size;
3913
+ upsertDiagnostics({
3914
+ pendingDelegations,
3915
+ pendingTimers
3916
+ });
3917
+ applySessionOperation("delegate", {
3918
+ pendingDelegations,
3919
+ pendingTimers
3920
+ });
3921
+ };
3922
+ runtimeHandle.emitWhenReady = (event, data, timeoutMs = 6e4, ack) => {
3923
+ return new Promise((resolve) => {
3924
+ const parsedTimeout = Number(timeoutMs);
3925
+ const normalizedTimeoutMs = Number.isFinite(parsedTimeout) && parsedTimeout > 0 ? Math.trunc(parsedTimeout) : 6e4;
3926
+ let timer = null;
3927
+ let settled = false;
3928
+ const clearPendingTimer = () => {
3929
+ if (!timer) {
3930
+ return;
3581
3931
  }
3582
- );
3583
- if (!waitResult.ok) {
3584
- CadenzaService.log(
3585
- waitResult.error,
3586
- { socketId: socket?.id, serviceName, URL, event },
3587
- "error"
3588
- );
3589
- this.recordSocketClientError(
3590
- fetchId,
3932
+ clearTimeout(timer);
3933
+ runtimeHandle.pendingTimers.delete(timer);
3934
+ syncPendingCounts();
3935
+ timer = null;
3936
+ };
3937
+ const settle = (response) => {
3938
+ if (settled) {
3939
+ return;
3940
+ }
3941
+ settled = true;
3942
+ clearPendingTimer();
3943
+ if (ack) ack(response);
3944
+ resolve(response);
3945
+ };
3946
+ const resolveWithError = (errorMessage, fallbackError) => {
3947
+ settle({
3948
+ ...data,
3949
+ errored: true,
3950
+ __error: errorMessage,
3951
+ error: fallbackError instanceof Error ? fallbackError.message : errorMessage,
3952
+ socketId: runtimeHandle.socket.id,
3591
3953
  serviceName,
3592
- URL,
3593
- waitResult.error
3594
- );
3595
- resolveWithError(waitResult.error);
3596
- return;
3597
- }
3598
- let timer = null;
3599
- if (timeoutMs !== 0) {
3600
- timer = setTimeout(() => {
3601
- if (timer) {
3602
- pendingTimers.delete(timer);
3603
- syncPendingCounts();
3604
- timer = null;
3954
+ url
3955
+ });
3956
+ };
3957
+ const tryEmit = async () => {
3958
+ const waitResult = await waitForSocketConnection(
3959
+ runtimeHandle.socket,
3960
+ normalizedTimeoutMs + 10,
3961
+ (reason, error) => {
3962
+ if (reason === "connect_timeout") {
3963
+ return `Socket connect timed out before '${event}'`;
3964
+ }
3965
+ if (reason === "connect_error") {
3966
+ const errMessage = error instanceof Error ? error.message : String(error);
3967
+ return `Socket connect error before '${event}': ${errMessage}`;
3968
+ }
3969
+ return `Socket disconnected before '${event}'`;
3605
3970
  }
3971
+ );
3972
+ if (!waitResult.ok) {
3606
3973
  CadenzaService.log(
3607
- `Socket event '${event}' timed out`,
3608
- { socketId: socket?.id, serviceName, URL },
3974
+ waitResult.error,
3975
+ {
3976
+ socketId: runtimeHandle.socket.id,
3977
+ serviceName,
3978
+ url,
3979
+ event
3980
+ },
3609
3981
  "error"
3610
3982
  );
3611
- this.recordSocketClientError(
3612
- fetchId,
3613
- serviceName,
3614
- URL,
3615
- `Socket event '${event}' timed out`
3616
- );
3617
- resolveWithError(`Socket event '${event}' timed out`);
3618
- }, timeoutMs + 10);
3619
- pendingTimers.add(timer);
3620
- syncPendingCounts();
3621
- }
3622
- const connectedSocket = socket;
3623
- if (!connectedSocket) {
3624
- resolveWithError(
3625
- `Socket unavailable before emitting '${event}'`
3626
- );
3627
- return;
3628
- }
3629
- connectedSocket.timeout(timeoutMs).emit(event, data, (err, response) => {
3630
- if (timer) {
3631
- clearTimeout(timer);
3632
- pendingTimers.delete(timer);
3633
- syncPendingCounts();
3634
- timer = null;
3983
+ upsertDiagnostics({}, waitResult.error);
3984
+ resolveWithError(waitResult.error);
3985
+ return;
3635
3986
  }
3636
- if (err) {
3987
+ timer = setTimeout(() => {
3988
+ if (settled) {
3989
+ return;
3990
+ }
3991
+ clearPendingTimer();
3992
+ const message = `Socket event '${event}' timed out`;
3637
3993
  CadenzaService.log(
3638
- "Socket timeout.",
3994
+ message,
3995
+ { socketId: runtimeHandle.socket.id, serviceName, url },
3996
+ "error"
3997
+ );
3998
+ upsertDiagnostics(
3639
3999
  {
3640
- event,
3641
- error: err.message,
3642
- socketId: socket?.id,
3643
- serviceName
4000
+ lastHandshakeError: message
3644
4001
  },
3645
- "warning"
4002
+ message
3646
4003
  );
3647
- this.recordSocketClientError(
3648
- fetchId,
4004
+ applySessionOperation("transmit", {
4005
+ lastHandshakeError: message
4006
+ });
4007
+ resolveWithError(message);
4008
+ }, normalizedTimeoutMs + 10);
4009
+ runtimeHandle.pendingTimers.add(timer);
4010
+ syncPendingCounts();
4011
+ runtimeHandle.socket.timeout(normalizedTimeoutMs).emit(event, data, (err, response) => {
4012
+ if (err) {
4013
+ CadenzaService.log(
4014
+ "Socket timeout.",
4015
+ {
4016
+ event,
4017
+ error: err.message,
4018
+ socketId: runtimeHandle.socket.id,
4019
+ serviceName
4020
+ },
4021
+ "warning"
4022
+ );
4023
+ upsertDiagnostics(
4024
+ {
4025
+ lastHandshakeError: err.message
4026
+ },
4027
+ err
4028
+ );
4029
+ applySessionOperation("transmit", {
4030
+ lastHandshakeError: err.message
4031
+ });
4032
+ response = {
4033
+ __error: `Timeout error: ${err}`,
4034
+ errored: true,
4035
+ ...data
4036
+ };
4037
+ }
4038
+ settle(response);
4039
+ });
4040
+ };
4041
+ void tryEmit().catch((error) => {
4042
+ CadenzaService.log(
4043
+ "Socket emit failed unexpectedly",
4044
+ {
4045
+ event,
4046
+ error: error instanceof Error ? error.message : String(error),
4047
+ socketId: runtimeHandle.socket.id,
3649
4048
  serviceName,
3650
- URL,
3651
- err
3652
- );
3653
- response = {
3654
- __error: `Timeout error: ${err}`,
3655
- errored: true,
3656
- ...ctx,
3657
- ...ctx.__metadata
3658
- };
3659
- }
3660
- if (ack) ack(response);
3661
- resolve(response);
4049
+ url
4050
+ },
4051
+ "error"
4052
+ );
4053
+ const message = `Socket event '${event}' failed`;
4054
+ upsertDiagnostics(
4055
+ {
4056
+ lastHandshakeError: error instanceof Error ? error.message : String(error)
4057
+ },
4058
+ error
4059
+ );
4060
+ applySessionOperation("transmit", {
4061
+ lastHandshakeError: error instanceof Error ? error.message : String(error)
4062
+ });
4063
+ resolveWithError(message, error);
3662
4064
  });
3663
- };
3664
- void tryEmit();
4065
+ });
4066
+ };
4067
+ const socket = runtimeHandle.socket;
4068
+ socket.on("connect", () => {
4069
+ if (runtimeHandle.handshake) return;
4070
+ upsertDiagnostics({
4071
+ connected: true,
4072
+ destroyed: false,
4073
+ socketId: socket.id ?? null
4074
+ });
4075
+ applySessionOperation("connect", {
4076
+ connected: true,
4077
+ destroyed: false,
4078
+ socketId: socket.id ?? null
4079
+ });
4080
+ CadenzaService.emit(`meta.socket_client.connected:${fetchId}`, input);
3665
4081
  });
3666
- };
3667
- socket.on("connect", () => {
3668
- if (handshake) return;
3669
- socketDiagnostics.connected = true;
3670
- socketDiagnostics.destroyed = false;
3671
- socketDiagnostics.socketId = socket?.id ?? null;
3672
- socketDiagnostics.updatedAt = Date.now();
3673
- CadenzaService.emit(`meta.socket_client.connected:${fetchId}`, ctx);
3674
- });
3675
- socket.on("delegation_progress", (ctx2) => {
3676
- CadenzaService.emit(
3677
- `meta.socket_client.delegation_progress:${ctx2.__metadata.__deputyExecId}`,
3678
- ctx2
3679
- );
3680
- });
3681
- socket.on("signal", (ctx2) => {
3682
- if (CadenzaService.signalBroker.listObservedSignals().includes(ctx2.__signalName)) {
3683
- CadenzaService.emit(ctx2.__signalName, ctx2);
3684
- }
3685
- });
3686
- socket.on("status_update", (status) => {
3687
- CadenzaService.emit("meta.socket_client.status_received", status);
3688
- });
3689
- socket.on("connect_error", (err) => {
3690
- handshake = false;
3691
- socketDiagnostics.connected = false;
3692
- socketDiagnostics.handshake = false;
3693
- socketDiagnostics.connectErrors++;
3694
- socketDiagnostics.lastHandshakeError = err.message;
3695
- socketDiagnostics.updatedAt = Date.now();
3696
- this.recordSocketClientError(fetchId, serviceName, URL, err);
3697
- CadenzaService.log(
3698
- "Socket connect error",
3699
- { error: err.message, serviceName, socketId: socket?.id, URL },
3700
- "error"
3701
- );
3702
- CadenzaService.emit(`meta.socket_client.connect_error:${fetchId}`, err);
3703
- });
3704
- socket.on("reconnect_attempt", (attempt) => {
3705
- socketDiagnostics.reconnectAttempts = Math.max(
3706
- socketDiagnostics.reconnectAttempts,
3707
- attempt
3708
- );
3709
- socketDiagnostics.updatedAt = Date.now();
3710
- CadenzaService.log(`Reconnect attempt: ${attempt}`);
3711
- });
3712
- socket.on("reconnect", (attempt) => {
3713
- socketDiagnostics.connected = true;
3714
- socketDiagnostics.updatedAt = Date.now();
3715
- CadenzaService.log(`Socket reconnected after ${attempt} tries`, {
3716
- socketId: socket?.id,
3717
- URL,
3718
- serviceName
4082
+ socket.on("delegation_progress", (delegationCtx) => {
4083
+ CadenzaService.emit(
4084
+ `meta.socket_client.delegation_progress:${delegationCtx.__metadata.__deputyExecId}`,
4085
+ delegationCtx
4086
+ );
3719
4087
  });
3720
- });
3721
- socket.on("reconnect_error", (err) => {
3722
- handshake = false;
3723
- socketDiagnostics.connected = false;
3724
- socketDiagnostics.handshake = false;
3725
- socketDiagnostics.reconnectErrors++;
3726
- socketDiagnostics.lastHandshakeError = err.message;
3727
- socketDiagnostics.updatedAt = Date.now();
3728
- this.recordSocketClientError(fetchId, serviceName, URL, err);
3729
- CadenzaService.log(
3730
- "Socket reconnect failed.",
3731
- { error: err.message, serviceName, URL, socketId: socket?.id },
3732
- "warning"
3733
- );
3734
- });
3735
- socket.on("error", (err) => {
3736
- errorCount++;
3737
- socketDiagnostics.socketErrors++;
3738
- socketDiagnostics.updatedAt = Date.now();
3739
- this.recordSocketClientError(fetchId, serviceName, URL, err);
3740
- CadenzaService.log(
3741
- "Socket error",
3742
- { error: err, socketId: socket?.id, URL, serviceName },
3743
- "error"
3744
- );
3745
- CadenzaService.emit("meta.socket_client.error", err);
3746
- });
3747
- socket.on("disconnect", () => {
3748
- const disconnectedAt = (/* @__PURE__ */ new Date()).toISOString();
3749
- socketDiagnostics.connected = false;
3750
- socketDiagnostics.handshake = false;
3751
- socketDiagnostics.lastDisconnectAt = disconnectedAt;
3752
- socketDiagnostics.updatedAt = Date.now();
3753
- CadenzaService.log(
3754
- "Socket disconnected.",
3755
- { URL, serviceName, socketId: socket?.id },
3756
- "warning"
3757
- );
3758
- CadenzaService.emit(`meta.socket_client.disconnected:${fetchId}`, {
3759
- serviceName,
3760
- serviceAddress,
3761
- servicePort
4088
+ socket.on("signal", (signalCtx) => {
4089
+ if (CadenzaService.signalBroker.listObservedSignals().includes(signalCtx.__signalName)) {
4090
+ CadenzaService.emit(signalCtx.__signalName, signalCtx);
4091
+ }
3762
4092
  });
3763
- handshake = false;
3764
- });
3765
- socket.connect();
3766
- handshakeTask = CadenzaService.createMetaTask(
3767
- `Socket handshake with ${URL}`,
3768
- async (ctx2, emit) => {
3769
- if (handshake) return;
3770
- handshake = true;
3771
- socketDiagnostics.handshake = true;
3772
- socketDiagnostics.updatedAt = Date.now();
3773
- await emitWhenReady?.(
3774
- "handshake",
4093
+ socket.on("status_update", (status) => {
4094
+ CadenzaService.emit("meta.socket_client.status_received", status);
4095
+ });
4096
+ socket.on("connect_error", (err) => {
4097
+ runtimeHandle.handshake = false;
4098
+ upsertDiagnostics(
3775
4099
  {
3776
- serviceInstanceId: CadenzaService.serviceRegistry.serviceInstanceId,
3777
- serviceName: CadenzaService.serviceRegistry.serviceName,
3778
- isFrontend: isBrowser,
3779
- __status: "success"
4100
+ connected: false,
4101
+ handshake: false,
4102
+ connectErrors: state.connectErrors + 1,
4103
+ lastHandshakeError: err.message
3780
4104
  },
3781
- 1e4,
3782
- (result) => {
3783
- if (result.status === "success") {
3784
- socketDiagnostics.connected = true;
3785
- socketDiagnostics.handshake = true;
3786
- socketDiagnostics.lastHandshakeAt = (/* @__PURE__ */ new Date()).toISOString();
3787
- socketDiagnostics.lastHandshakeError = null;
3788
- socketDiagnostics.updatedAt = Date.now();
3789
- CadenzaService.log("Socket client connected", {
3790
- result,
3791
- serviceName,
3792
- socketId: socket?.id,
3793
- URL
3794
- });
3795
- } else {
3796
- socketDiagnostics.connected = false;
3797
- socketDiagnostics.handshake = false;
3798
- socketDiagnostics.lastHandshakeError = result?.__error ?? result?.error ?? "Socket handshake failed";
3799
- socketDiagnostics.updatedAt = Date.now();
3800
- this.recordSocketClientError(
3801
- fetchId,
3802
- serviceName,
3803
- URL,
3804
- socketDiagnostics.lastHandshakeError
3805
- );
3806
- CadenzaService.log(
3807
- "Socket handshake failed",
3808
- { result, serviceName, socketId: socket?.id, URL },
3809
- "warning"
3810
- );
3811
- }
3812
- }
4105
+ err
3813
4106
  );
3814
- },
3815
- "Handshakes with socket server"
3816
- ).doOn(`meta.socket_client.connected:${fetchId}`);
3817
- delegateTask = CadenzaService.createMetaTask(
3818
- `Delegate flow to Socket service ${URL}`,
3819
- async (ctx2, emit) => {
3820
- if (ctx2.__remoteRoutineName === void 0) {
3821
- return;
3822
- }
3823
- return new Promise((resolve) => {
3824
- delete ctx2.__isSubMeta;
3825
- delete ctx2.__broadcast;
3826
- const requestSentAt = Date.now();
3827
- pendingDelegationIds.add(ctx2.__metadata.__deputyExecId);
3828
- syncPendingCounts();
3829
- emitWhenReady?.(
3830
- "delegation",
3831
- ctx2,
3832
- ctx2.__timeout ?? 6e4,
3833
- (resultContext) => {
3834
- const requestDuration = Date.now() - requestSentAt;
3835
- const metadata = resultContext.__metadata;
3836
- delete resultContext.__metadata;
3837
- emit(
3838
- `meta.socket_client.delegated:${ctx2.__metadata.__deputyExecId}`,
3839
- {
3840
- ...resultContext,
3841
- ...metadata,
3842
- __requestDuration: requestDuration
3843
- }
3844
- );
3845
- pendingDelegationIds.delete(ctx2.__metadata.__deputyExecId);
3846
- syncPendingCounts();
3847
- if (resultContext?.errored || resultContext?.failed) {
3848
- this.recordSocketClientError(
3849
- fetchId,
4107
+ applySessionOperation("connect", {
4108
+ connected: false,
4109
+ handshake: false,
4110
+ connectErrors: state.connectErrors + 1,
4111
+ lastHandshakeError: err.message
4112
+ });
4113
+ CadenzaService.log(
4114
+ "Socket connect error",
4115
+ {
4116
+ error: err.message,
4117
+ serviceName,
4118
+ socketId: socket.id,
4119
+ url
4120
+ },
4121
+ "error"
4122
+ );
4123
+ CadenzaService.emit(`meta.socket_client.connect_error:${fetchId}`, err);
4124
+ });
4125
+ socket.on("reconnect_attempt", (attempt) => {
4126
+ upsertDiagnostics({ reconnectAttempts: attempt });
4127
+ applySessionOperation("connect", {
4128
+ reconnectAttempts: attempt
4129
+ });
4130
+ CadenzaService.log(`Reconnect attempt: ${attempt}`);
4131
+ });
4132
+ socket.on("reconnect", (attempt) => {
4133
+ upsertDiagnostics({ connected: true });
4134
+ applySessionOperation("connect", {
4135
+ connected: true
4136
+ });
4137
+ CadenzaService.log(`Socket reconnected after ${attempt} tries`, {
4138
+ socketId: socket.id,
4139
+ url,
4140
+ serviceName
4141
+ });
4142
+ });
4143
+ socket.on("reconnect_error", (err) => {
4144
+ runtimeHandle.handshake = false;
4145
+ upsertDiagnostics(
4146
+ {
4147
+ connected: false,
4148
+ handshake: false,
4149
+ reconnectErrors: state.reconnectErrors + 1,
4150
+ lastHandshakeError: err.message
4151
+ },
4152
+ err
4153
+ );
4154
+ applySessionOperation("connect", {
4155
+ connected: false,
4156
+ handshake: false,
4157
+ reconnectErrors: state.reconnectErrors + 1,
4158
+ lastHandshakeError: err.message
4159
+ });
4160
+ CadenzaService.log(
4161
+ "Socket reconnect failed.",
4162
+ { error: err.message, serviceName, url, socketId: socket.id },
4163
+ "warning"
4164
+ );
4165
+ });
4166
+ socket.on("error", (err) => {
4167
+ runtimeHandle.errorCount += 1;
4168
+ upsertDiagnostics(
4169
+ {
4170
+ socketErrors: state.socketErrors + 1,
4171
+ lastHandshakeError: this.getErrorMessage(err)
4172
+ },
4173
+ err
4174
+ );
4175
+ applySessionOperation("transmit", {
4176
+ socketErrors: state.socketErrors + 1,
4177
+ errorCount: runtimeHandle.errorCount,
4178
+ lastHandshakeError: this.getErrorMessage(err)
4179
+ });
4180
+ CadenzaService.log(
4181
+ "Socket error",
4182
+ { error: err, socketId: socket.id, url, serviceName },
4183
+ "error"
4184
+ );
4185
+ CadenzaService.emit("meta.socket_client.error", err);
4186
+ });
4187
+ socket.on("disconnect", () => {
4188
+ const disconnectedAt = (/* @__PURE__ */ new Date()).toISOString();
4189
+ upsertDiagnostics({
4190
+ connected: false,
4191
+ handshake: false,
4192
+ lastDisconnectAt: disconnectedAt
4193
+ });
4194
+ applySessionOperation("connect", {
4195
+ connected: false,
4196
+ handshake: false,
4197
+ lastDisconnectAt: disconnectedAt
4198
+ });
4199
+ CadenzaService.log(
4200
+ "Socket disconnected.",
4201
+ { url, serviceName, socketId: socket.id },
4202
+ "warning"
4203
+ );
4204
+ CadenzaService.emit(`meta.socket_client.disconnected:${fetchId}`, {
4205
+ serviceName,
4206
+ serviceAddress,
4207
+ servicePort: normalizedPort
4208
+ });
4209
+ runtimeHandle.handshake = false;
4210
+ });
4211
+ socket.connect();
4212
+ runtimeHandle.handshakeTask = CadenzaService.createMetaTask(
4213
+ `Socket handshake with ${url}`,
4214
+ async (_ctx, emitter) => {
4215
+ if (runtimeHandle.handshake) return;
4216
+ runtimeHandle.handshake = true;
4217
+ upsertDiagnostics({
4218
+ handshake: true
4219
+ });
4220
+ applySessionOperation("handshake", {
4221
+ handshake: true
4222
+ });
4223
+ await runtimeHandle.emitWhenReady?.(
4224
+ "handshake",
4225
+ {
4226
+ serviceInstanceId: CadenzaService.serviceRegistry.serviceInstanceId,
4227
+ serviceName: CadenzaService.serviceRegistry.serviceName,
4228
+ isFrontend: isBrowser,
4229
+ __status: "success"
4230
+ },
4231
+ 1e4,
4232
+ (result) => {
4233
+ if (result.status === "success") {
4234
+ const handshakeAt = (/* @__PURE__ */ new Date()).toISOString();
4235
+ upsertDiagnostics({
4236
+ connected: true,
4237
+ handshake: true,
4238
+ lastHandshakeAt: handshakeAt,
4239
+ lastHandshakeError: null,
4240
+ socketId: socket.id ?? null
4241
+ });
4242
+ applySessionOperation("handshake", {
4243
+ connected: true,
4244
+ handshake: true,
4245
+ lastHandshakeAt: handshakeAt,
4246
+ lastHandshakeError: null,
4247
+ socketId: socket.id ?? null
4248
+ });
4249
+ CadenzaService.log("Socket client connected", {
4250
+ result,
3850
4251
  serviceName,
3851
- URL,
3852
- resultContext?.__error ?? resultContext?.error ?? "Socket delegation failed"
4252
+ socketId: socket.id,
4253
+ url
4254
+ });
4255
+ } else {
4256
+ const errorMessage = result?.__error ?? result?.error ?? "Socket handshake failed";
4257
+ upsertDiagnostics(
4258
+ {
4259
+ connected: false,
4260
+ handshake: false,
4261
+ lastHandshakeError: errorMessage
4262
+ },
4263
+ errorMessage
4264
+ );
4265
+ applySessionOperation("handshake", {
4266
+ connected: false,
4267
+ handshake: false,
4268
+ lastHandshakeError: errorMessage
4269
+ });
4270
+ CadenzaService.log(
4271
+ "Socket handshake failed",
4272
+ { result, serviceName, socketId: socket.id, url },
4273
+ "warning"
3853
4274
  );
3854
4275
  }
3855
- resolve(resultContext);
4276
+ void emitter;
3856
4277
  }
3857
4278
  );
3858
- });
3859
- },
3860
- `Delegate flow to service ${serviceName} with address ${URL}`
3861
- ).doOn(`meta.service_registry.selected_instance_for_socket:${fetchId}`).attachSignal(
3862
- "meta.socket_client.delegated",
3863
- "meta.socket_shutdown_requested"
3864
- );
3865
- transmitTask = CadenzaService.createMetaTask(
3866
- `Transmit signal to socket server ${URL}`,
3867
- async (ctx2, emit) => {
3868
- if (ctx2.__signalName === void 0) {
3869
- return;
3870
- }
3871
- return new Promise((resolve) => {
3872
- delete ctx2.__broadcast;
3873
- emitWhenReady?.("signal", ctx2, 5e3, (response) => {
3874
- if (ctx2.__routineExecId) {
3875
- emit(
3876
- `meta.socket_client.transmitted:${ctx2.__routineExecId}`,
3877
- response
4279
+ },
4280
+ "Handshakes with socket server"
4281
+ ).doOn(`meta.socket_client.connected:${fetchId}`);
4282
+ runtimeHandle.delegateTask = CadenzaService.createMetaTask(
4283
+ `Delegate flow to Socket service ${url}`,
4284
+ async (delegateCtx, emitter) => {
4285
+ if (delegateCtx.__remoteRoutineName === void 0) {
4286
+ return;
4287
+ }
4288
+ delete delegateCtx.__isSubMeta;
4289
+ delete delegateCtx.__broadcast;
4290
+ const deputyExecId = delegateCtx.__metadata?.__deputyExecId;
4291
+ const requestSentAt = Date.now();
4292
+ if (deputyExecId) {
4293
+ runtimeHandle.pendingDelegationIds.add(deputyExecId);
4294
+ syncPendingCounts();
4295
+ }
4296
+ try {
4297
+ const resultContext = await runtimeHandle.emitWhenReady?.(
4298
+ "delegation",
4299
+ delegateCtx,
4300
+ delegateCtx.__timeout ?? 6e4
4301
+ ) ?? {
4302
+ errored: true,
4303
+ __error: "Socket delegation returned no response"
4304
+ };
4305
+ const requestDuration = Date.now() - requestSentAt;
4306
+ const metadata = resultContext.__metadata;
4307
+ delete resultContext.__metadata;
4308
+ if (deputyExecId) {
4309
+ emitter(`meta.socket_client.delegated:${deputyExecId}`, {
4310
+ ...resultContext,
4311
+ ...metadata,
4312
+ __requestDuration: requestDuration
4313
+ });
4314
+ }
4315
+ if (resultContext?.errored || resultContext?.failed) {
4316
+ const errorMessage = resultContext?.__error ?? resultContext?.error ?? "Socket delegation failed";
4317
+ upsertDiagnostics(
4318
+ {
4319
+ lastHandshakeError: String(errorMessage)
4320
+ },
4321
+ errorMessage
3878
4322
  );
4323
+ applySessionOperation("delegate", {
4324
+ lastHandshakeError: String(errorMessage)
4325
+ });
4326
+ }
4327
+ return resultContext;
4328
+ } catch (error) {
4329
+ const message = error instanceof Error ? error.message : String(error);
4330
+ const failedContext = {
4331
+ errored: true,
4332
+ __error: message
4333
+ };
4334
+ if (deputyExecId) {
4335
+ emitter(`meta.socket_client.delegated:${deputyExecId}`, {
4336
+ ...failedContext,
4337
+ __requestDuration: Date.now() - requestSentAt
4338
+ });
4339
+ }
4340
+ upsertDiagnostics(
4341
+ {
4342
+ lastHandshakeError: message
4343
+ },
4344
+ error
4345
+ );
4346
+ applySessionOperation("delegate", {
4347
+ lastHandshakeError: message
4348
+ });
4349
+ return failedContext;
4350
+ } finally {
4351
+ if (deputyExecId) {
4352
+ runtimeHandle.pendingDelegationIds.delete(deputyExecId);
4353
+ syncPendingCounts();
3879
4354
  }
3880
- resolve(response);
3881
- });
3882
- });
3883
- },
3884
- `Transmits signal to service ${serviceName} with address ${URL}`
3885
- ).doOn(`meta.service_registry.selected_instance_for_socket:${fetchId}`).attachSignal("meta.socket_client.transmitted");
3886
- CadenzaService.createEphemeralMetaTask(
3887
- `Shutdown SocketClient ${URL}`,
3888
- (ctx2, emit) => {
3889
- handshake = false;
3890
- socketDiagnostics.connected = false;
3891
- socketDiagnostics.handshake = false;
3892
- socketDiagnostics.destroyed = true;
3893
- socketDiagnostics.updatedAt = Date.now();
3894
- CadenzaService.log("Shutting down socket client", { URL, serviceName });
3895
- socket?.close();
3896
- handshakeTask?.destroy();
3897
- delegateTask?.destroy();
3898
- transmitTask?.destroy();
3899
- handshakeTask = null;
3900
- delegateTask = null;
3901
- transmitTask = null;
3902
- emitWhenReady = null;
3903
- socket = null;
3904
- emit(`meta.fetch.handshake_requested:${fetchId}`, {
3905
- serviceInstanceId,
3906
- serviceName,
3907
- communicationTypes,
3908
- serviceAddress,
3909
- servicePort,
3910
- protocol,
3911
- handshakeData: {
3912
- instanceId: CadenzaService.serviceRegistry.serviceInstanceId,
3913
- serviceName: CadenzaService.serviceRegistry.serviceName
3914
4355
  }
3915
- });
3916
- for (const id of pendingDelegationIds) {
3917
- emit(`meta.socket_client.delegated:${id}`, {
4356
+ },
4357
+ `Delegate flow to service ${serviceName} with address ${url}`
4358
+ ).doOn(`meta.service_registry.selected_instance_for_socket:${fetchId}`).attachSignal(
4359
+ "meta.socket_client.delegated",
4360
+ "meta.socket_shutdown_requested"
4361
+ );
4362
+ runtimeHandle.transmitTask = CadenzaService.createMetaTask(
4363
+ `Transmit signal to socket server ${url}`,
4364
+ async (signalCtx, emitter) => {
4365
+ if (signalCtx.__signalName === void 0) {
4366
+ return;
4367
+ }
4368
+ delete signalCtx.__broadcast;
4369
+ const response = await runtimeHandle.emitWhenReady?.("signal", signalCtx, 5e3) ?? {
3918
4370
  errored: true,
3919
- __error: "Shutting down socket client"
4371
+ __error: "Socket signal transmission returned no response"
4372
+ };
4373
+ applySessionOperation("transmit", {});
4374
+ if (signalCtx.__routineExecId) {
4375
+ emitter(`meta.socket_client.transmitted:${signalCtx.__routineExecId}`, {
4376
+ ...response
4377
+ });
4378
+ }
4379
+ return response;
4380
+ },
4381
+ `Transmits signal to service ${serviceName} with address ${url}`
4382
+ ).doOn(`meta.service_registry.selected_instance_for_socket:${fetchId}`).attachSignal("meta.socket_client.transmitted");
4383
+ CadenzaService.createEphemeralMetaTask(
4384
+ `Shutdown SocketClient ${url}`,
4385
+ (_ctx, emitter) => {
4386
+ runtimeHandle.handshake = false;
4387
+ upsertDiagnostics({
4388
+ connected: false,
4389
+ handshake: false,
4390
+ destroyed: true,
4391
+ pendingDelegations: 0,
4392
+ pendingTimers: 0
3920
4393
  });
3921
- }
3922
- pendingDelegationIds.clear();
3923
- syncPendingCounts();
3924
- for (const timer of pendingTimers) {
3925
- clearTimeout(timer);
3926
- }
3927
- pendingTimers.clear();
3928
- syncPendingCounts();
3929
- },
3930
- "Shuts down the socket client"
3931
- ).doOn(
3932
- `meta.socket_shutdown_requested:${fetchId}`,
3933
- `meta.socket_client.disconnected:${fetchId}`,
3934
- `meta.fetch.handshake_failed:${fetchId}`,
3935
- `meta.socket_client.connect_error:${fetchId}`
3936
- ).attachSignal("meta.fetch.handshake_requested").emits("meta.socket_client_shutdown_complete");
3937
- return true;
3938
- },
3939
- "Connects to a specified socket server"
4394
+ applySessionOperation("shutdown", {
4395
+ connected: false,
4396
+ handshake: false,
4397
+ destroyed: true,
4398
+ pendingDelegations: 0,
4399
+ pendingTimers: 0
4400
+ });
4401
+ CadenzaService.log("Shutting down socket client", { url, serviceName });
4402
+ emitter(`meta.fetch.handshake_requested:${fetchId}`, {
4403
+ serviceInstanceId,
4404
+ serviceName,
4405
+ communicationTypes,
4406
+ serviceAddress,
4407
+ servicePort: normalizedPort,
4408
+ protocol,
4409
+ handshakeData: {
4410
+ instanceId: CadenzaService.serviceRegistry.serviceInstanceId,
4411
+ serviceName: CadenzaService.serviceRegistry.serviceName
4412
+ }
4413
+ });
4414
+ for (const id of runtimeHandle.pendingDelegationIds) {
4415
+ emitter(`meta.socket_client.delegated:${id}`, {
4416
+ errored: true,
4417
+ __error: "Shutting down socket client"
4418
+ });
4419
+ }
4420
+ this.destroySocketClientRuntimeHandle(runtimeHandle);
4421
+ emitter("meta.socket_client.runtime_clear_requested", {
4422
+ fetchId
4423
+ });
4424
+ },
4425
+ "Shuts down the socket client"
4426
+ ).doOn(
4427
+ `meta.socket_shutdown_requested:${fetchId}`,
4428
+ `meta.socket_client.disconnected:${fetchId}`,
4429
+ `meta.fetch.handshake_failed:${fetchId}`,
4430
+ `meta.socket_client.connect_error:${fetchId}`
4431
+ ).attachSignal("meta.fetch.handshake_requested").emits("meta.socket_client_shutdown_complete");
4432
+ return true;
4433
+ },
4434
+ { mode: "write" }
4435
+ ),
4436
+ "Connects to a specified socket server and wires runtime tasks."
3940
4437
  ).doOn("meta.fetch.handshake_complete").emitsOnFail("meta.socket_client.connect_failed");
3941
4438
  }
3942
- static get instance() {
3943
- if (!this._instance) this._instance = new _SocketController();
3944
- return this._instance;
4439
+ resolveSocketServerKey(input) {
4440
+ return String(input.serverKey ?? input.__socketServerKey ?? this.socketServerDefaultKey).trim() || this.socketServerDefaultKey;
3945
4441
  }
3946
- resolveTransportDiagnosticsOptions(ctx) {
3947
- const detailLevel = ctx.detailLevel === "full" ? "full" : "summary";
3948
- const includeErrorHistory = Boolean(ctx.includeErrorHistory);
3949
- const requestedLimit = Number(ctx.errorHistoryLimit);
3950
- const errorHistoryLimit = Number.isFinite(requestedLimit) ? Math.max(1, Math.min(200, Math.trunc(requestedLimit))) : 10;
4442
+ resolveSocketClientFetchId(input) {
4443
+ const explicitFetchId = String(input.fetchId ?? "").trim();
4444
+ if (explicitFetchId) {
4445
+ return explicitFetchId;
4446
+ }
4447
+ const serviceAddress = String(input.serviceAddress ?? "").trim();
4448
+ const protocol = String(input.protocol ?? "http").trim();
4449
+ const port = this.resolveServicePort(protocol, input.servicePort);
4450
+ if (!serviceAddress || !port) {
4451
+ return void 0;
4452
+ }
4453
+ return `${serviceAddress}_${port}`;
4454
+ }
4455
+ resolveServicePort(protocol, rawPort) {
4456
+ if (protocol === "https") {
4457
+ return 443;
4458
+ }
4459
+ const parsed = Number(rawPort);
4460
+ if (!Number.isFinite(parsed) || parsed <= 0) {
4461
+ return void 0;
4462
+ }
4463
+ return Math.trunc(parsed);
4464
+ }
4465
+ createSocketServerRuntimeHandleFromContext(context) {
4466
+ const baseServer = context.httpsServer ?? context.httpServer;
4467
+ if (!baseServer) {
4468
+ throw new Error(
4469
+ "Socket server runtime setup requires either httpsServer or httpServer"
4470
+ );
4471
+ }
4472
+ const server = new Server(baseServer, {
4473
+ pingInterval: 3e4,
4474
+ pingTimeout: 2e4,
4475
+ maxHttpBufferSize: 1e7,
4476
+ connectionStateRecovery: {
4477
+ maxDisconnectionDuration: 2 * 60 * 1e3,
4478
+ skipMiddlewares: true
4479
+ }
4480
+ });
3951
4481
  return {
3952
- detailLevel,
3953
- includeErrorHistory,
3954
- errorHistoryLimit
4482
+ server,
4483
+ initialized: false,
4484
+ connectedSocketIds: /* @__PURE__ */ new Set(),
4485
+ broadcastStatusTask: null,
4486
+ shutdownTask: null
3955
4487
  };
3956
4488
  }
3957
- ensureSocketClientDiagnostics(fetchId, serviceName, url) {
3958
- let state = this.socketClientDiagnostics.get(fetchId);
3959
- if (!state) {
3960
- state = {
3961
- fetchId,
3962
- serviceName,
3963
- url,
3964
- socketId: null,
3965
- connected: false,
3966
- handshake: false,
3967
- reconnectAttempts: 0,
3968
- connectErrors: 0,
3969
- reconnectErrors: 0,
3970
- socketErrors: 0,
3971
- pendingDelegations: 0,
3972
- pendingTimers: 0,
3973
- destroyed: false,
3974
- lastHandshakeAt: null,
3975
- lastHandshakeError: null,
3976
- lastDisconnectAt: null,
3977
- lastError: null,
3978
- lastErrorAt: 0,
3979
- errorHistory: [],
3980
- updatedAt: Date.now()
3981
- };
3982
- this.socketClientDiagnostics.set(fetchId, state);
3983
- } else {
3984
- state.serviceName = serviceName;
3985
- state.url = url;
4489
+ destroySocketServerRuntimeHandle(runtimeHandle) {
4490
+ if (!runtimeHandle) {
4491
+ return;
3986
4492
  }
3987
- return state;
4493
+ runtimeHandle.broadcastStatusTask?.destroy();
4494
+ runtimeHandle.shutdownTask?.destroy();
4495
+ runtimeHandle.broadcastStatusTask = null;
4496
+ runtimeHandle.shutdownTask = null;
4497
+ runtimeHandle.connectedSocketIds.clear();
4498
+ runtimeHandle.initialized = false;
4499
+ runtimeHandle.server.close();
4500
+ runtimeHandle.server.removeAllListeners();
4501
+ }
4502
+ createSocketClientRuntimeHandle(url) {
4503
+ return {
4504
+ url,
4505
+ socket: io(url, {
4506
+ reconnection: true,
4507
+ reconnectionAttempts: 5,
4508
+ reconnectionDelay: 2e3,
4509
+ reconnectionDelayMax: 1e4,
4510
+ randomizationFactor: 0.5,
4511
+ transports: ["websocket"],
4512
+ autoConnect: false
4513
+ }),
4514
+ initialized: false,
4515
+ handshake: false,
4516
+ errorCount: 0,
4517
+ pendingDelegationIds: /* @__PURE__ */ new Set(),
4518
+ pendingTimers: /* @__PURE__ */ new Set(),
4519
+ emitWhenReady: null,
4520
+ handshakeTask: null,
4521
+ delegateTask: null,
4522
+ transmitTask: null
4523
+ };
4524
+ }
4525
+ destroySocketClientRuntimeHandle(runtimeHandle) {
4526
+ if (!runtimeHandle) {
4527
+ return;
4528
+ }
4529
+ runtimeHandle.initialized = false;
4530
+ runtimeHandle.handshake = false;
4531
+ runtimeHandle.emitWhenReady = null;
4532
+ runtimeHandle.handshakeTask?.destroy();
4533
+ runtimeHandle.delegateTask?.destroy();
4534
+ runtimeHandle.transmitTask?.destroy();
4535
+ runtimeHandle.handshakeTask = null;
4536
+ runtimeHandle.delegateTask = null;
4537
+ runtimeHandle.transmitTask = null;
4538
+ for (const timer of runtimeHandle.pendingTimers) {
4539
+ clearTimeout(timer);
4540
+ }
4541
+ runtimeHandle.pendingTimers.clear();
4542
+ runtimeHandle.pendingDelegationIds.clear();
4543
+ runtimeHandle.socket.close();
4544
+ runtimeHandle.socket.removeAllListeners();
4545
+ }
4546
+ normalizeCommunicationTypes(value) {
4547
+ if (!Array.isArray(value)) {
4548
+ return [];
4549
+ }
4550
+ return value.map((item) => String(item)).filter((item) => item.trim().length > 0);
3988
4551
  }
3989
4552
  getErrorMessage(error) {
3990
4553
  if (error instanceof Error) {
@@ -3999,28 +4562,53 @@ var SocketController = class _SocketController {
3999
4562
  return String(error);
4000
4563
  }
4001
4564
  }
4002
- recordSocketClientError(fetchId, serviceName, url, error) {
4003
- const state = this.ensureSocketClientDiagnostics(fetchId, serviceName, url);
4004
- const message = this.getErrorMessage(error);
4005
- const now = Date.now();
4006
- state.lastError = message;
4007
- state.lastErrorAt = now;
4008
- state.updatedAt = now;
4009
- state.errorHistory.push({
4010
- at: new Date(now).toISOString(),
4011
- message
4565
+ pruneDiagnosticsEntries(entries, now = Date.now()) {
4566
+ for (const [fetchId, state] of Object.entries(entries)) {
4567
+ if (state.destroyed && now - state.updatedAt > this.destroyedDiagnosticsTtlMs) {
4568
+ delete entries[fetchId];
4569
+ }
4570
+ }
4571
+ if (Object.keys(entries).length <= this.diagnosticsMaxClientEntries) {
4572
+ return;
4573
+ }
4574
+ const entriesByEvictionPriority = Object.entries(entries).sort((left, right) => {
4575
+ if (left[1].destroyed !== right[1].destroyed) {
4576
+ return left[1].destroyed ? -1 : 1;
4577
+ }
4578
+ return left[1].updatedAt - right[1].updatedAt;
4012
4579
  });
4013
- if (state.errorHistory.length > this.diagnosticsErrorHistoryLimit) {
4014
- state.errorHistory.splice(
4015
- 0,
4016
- state.errorHistory.length - this.diagnosticsErrorHistoryLimit
4017
- );
4580
+ while (Object.keys(entries).length > this.diagnosticsMaxClientEntries && entriesByEvictionPriority.length > 0) {
4581
+ const [fetchId] = entriesByEvictionPriority.shift();
4582
+ delete entries[fetchId];
4583
+ }
4584
+ }
4585
+ async getSocketClientDiagnosticsEntry(fetchId) {
4586
+ const normalized = String(fetchId ?? "").trim();
4587
+ if (!normalized) {
4588
+ return void 0;
4018
4589
  }
4590
+ const snapshot = this.socketClientDiagnosticsActor.getState();
4591
+ const entries = { ...snapshot.entries };
4592
+ this.pruneDiagnosticsEntries(entries);
4593
+ return entries[normalized];
4594
+ }
4595
+ resolveTransportDiagnosticsOptions(ctx) {
4596
+ const detailLevel = ctx.detailLevel === "full" ? "full" : "summary";
4597
+ const includeErrorHistory = Boolean(ctx.includeErrorHistory);
4598
+ const requestedLimit = Number(ctx.errorHistoryLimit);
4599
+ const errorHistoryLimit = Number.isFinite(requestedLimit) ? Math.max(1, Math.min(200, Math.trunc(requestedLimit))) : 10;
4600
+ return {
4601
+ detailLevel,
4602
+ includeErrorHistory,
4603
+ errorHistoryLimit
4604
+ };
4019
4605
  }
4020
- collectSocketTransportDiagnostics(ctx) {
4606
+ collectSocketTransportDiagnostics(ctx, diagnosticsEntries) {
4021
4607
  const { detailLevel, includeErrorHistory, errorHistoryLimit } = this.resolveTransportDiagnosticsOptions(ctx);
4022
4608
  const serviceName = CadenzaService.serviceRegistry.serviceName ?? "UnknownService";
4023
- const states = Array.from(this.socketClientDiagnostics.values()).sort(
4609
+ const entries = { ...diagnosticsEntries };
4610
+ this.pruneDiagnosticsEntries(entries);
4611
+ const states = Object.values(entries).sort(
4024
4612
  (a, b) => a.fetchId.localeCompare(b.fetchId)
4025
4613
  );
4026
4614
  const summary = {
@@ -4038,10 +4626,7 @@ var SocketController = class _SocketController {
4038
4626
  0
4039
4627
  ),
4040
4628
  connectErrors: states.reduce((acc, state) => acc + state.connectErrors, 0),
4041
- reconnectErrors: states.reduce(
4042
- (acc, state) => acc + state.reconnectErrors,
4043
- 0
4044
- ),
4629
+ reconnectErrors: states.reduce((acc, state) => acc + state.reconnectErrors, 0),
4045
4630
  socketErrors: states.reduce((acc, state) => acc + state.socketErrors, 0),
4046
4631
  latestError: states.slice().sort((a, b) => b.lastErrorAt - a.lastErrorAt).find((state) => state.lastError)?.lastError ?? null
4047
4632
  };
@@ -4400,6 +4985,22 @@ var GraphMetadataController = class _GraphMetadataController {
4400
4985
  "Handles task execution relationship creation",
4401
4986
  { concurrency: 100, isSubMeta: true }
4402
4987
  ).doOn("meta.node.mapped", "meta.node.detected_previous_task_execution").emits("global.meta.graph_metadata.relationship_executed");
4988
+ CadenzaService.createMetaTask("Handle actor creation", (ctx) => {
4989
+ return {
4990
+ data: {
4991
+ ...ctx.data,
4992
+ service_name: CadenzaService.serviceRegistry.serviceName
4993
+ }
4994
+ };
4995
+ }).doOn("meta.actor.created").emits("global.meta.graph_metadata.actor_created");
4996
+ CadenzaService.createMetaTask("Handle actor task association", (ctx) => {
4997
+ return {
4998
+ data: {
4999
+ ...ctx.data,
5000
+ service_name: CadenzaService.serviceRegistry.serviceName
5001
+ }
5002
+ };
5003
+ }).doOn("meta.actor.task_associated").emits("global.meta.graph_metadata.actor_task_associated");
4403
5004
  CadenzaService.createMetaTask("Handle Intent Creation", (ctx) => {
4404
5005
  const intentName = ctx.data?.name;
4405
5006
  return {
@@ -6071,8 +6672,69 @@ function tableFieldTypeToSchemaType(type) {
6071
6672
  import { v4 as uuid3 } from "uuid";
6072
6673
 
6073
6674
  // src/graph/controllers/GraphSyncController.ts
6675
+ var ACTOR_TASK_METADATA = /* @__PURE__ */ Symbol.for("@cadenza.io/core/actor-task-meta");
6676
+ function getActorTaskRuntimeMetadata(taskFunction) {
6677
+ if (typeof taskFunction !== "function") {
6678
+ return void 0;
6679
+ }
6680
+ return taskFunction[ACTOR_TASK_METADATA];
6681
+ }
6682
+ function sanitizeActorMetadataValue(value) {
6683
+ if (value === null) {
6684
+ return null;
6685
+ }
6686
+ if (value === void 0 || typeof value === "function") {
6687
+ return void 0;
6688
+ }
6689
+ if (Array.isArray(value)) {
6690
+ const items = [];
6691
+ for (const item of value) {
6692
+ const sanitizedItem = sanitizeActorMetadataValue(item);
6693
+ if (sanitizedItem !== void 0) {
6694
+ items.push(sanitizedItem);
6695
+ }
6696
+ }
6697
+ return items;
6698
+ }
6699
+ if (typeof value === "object") {
6700
+ const output = {};
6701
+ for (const [key, nestedValue] of Object.entries(value)) {
6702
+ const sanitizedNestedValue = sanitizeActorMetadataValue(nestedValue);
6703
+ if (sanitizedNestedValue !== void 0) {
6704
+ output[key] = sanitizedNestedValue;
6705
+ }
6706
+ }
6707
+ return output;
6708
+ }
6709
+ return value;
6710
+ }
6711
+ function buildActorRegistrationData(actor) {
6712
+ const definition = sanitizeActorMetadataValue(
6713
+ typeof actor?.toDefinition === "function" ? actor.toDefinition() : {}
6714
+ );
6715
+ const stateDefinition = definition?.state && typeof definition.state === "object" ? definition.state : {};
6716
+ const actorKind = typeof definition?.kind === "string" ? definition.kind : actor?.kind;
6717
+ return {
6718
+ name: definition?.name ?? actor?.spec?.name ?? "",
6719
+ description: definition?.description ?? actor?.spec?.description ?? "",
6720
+ default_key: definition?.defaultKey ?? actor?.spec?.defaultKey ?? "default",
6721
+ load_policy: definition?.loadPolicy ?? actor?.spec?.loadPolicy ?? "eager",
6722
+ write_contract: definition?.writeContract ?? actor?.spec?.writeContract ?? "overwrite",
6723
+ runtime_read_guard: definition?.runtimeReadGuard ?? actor?.spec?.runtimeReadGuard ?? "none",
6724
+ consistency_profile: definition?.consistencyProfile ?? actor?.spec?.consistencyProfile ?? null,
6725
+ key_definition: definition?.key ?? null,
6726
+ state_definition: stateDefinition,
6727
+ retry_policy: definition?.retry ?? {},
6728
+ idempotency_policy: definition?.idempotency ?? {},
6729
+ session_policy: definition?.session ?? {},
6730
+ is_meta: actorKind === "meta",
6731
+ version: 1
6732
+ };
6733
+ }
6074
6734
  var GraphSyncController = class _GraphSyncController {
6075
6735
  constructor() {
6736
+ this.registeredActors = /* @__PURE__ */ new Set();
6737
+ this.registeredActorTaskMaps = /* @__PURE__ */ new Set();
6076
6738
  this.isCadenzaDBReady = false;
6077
6739
  }
6078
6740
  static get instance() {
@@ -6330,6 +6992,120 @@ var GraphSyncController = class _GraphSyncController {
6330
6992
  )
6331
6993
  )
6332
6994
  );
6995
+ this.splitActorsForRegistration = CadenzaService.createMetaTask(
6996
+ "Split actors for registration",
6997
+ function* (ctx) {
6998
+ CadenzaService.debounce("meta.sync_controller.synced_resource", {
6999
+ delayMs: 3e3
7000
+ });
7001
+ const actors = ctx.actors ?? [];
7002
+ for (const actor of actors) {
7003
+ const data = {
7004
+ ...buildActorRegistrationData(actor),
7005
+ service_name: CadenzaService.serviceRegistry.serviceName
7006
+ };
7007
+ if (!data.name) {
7008
+ continue;
7009
+ }
7010
+ const registrationKey = `${data.name}|${data.version}|${data.service_name}`;
7011
+ if (this.registeredActors.has(registrationKey)) {
7012
+ continue;
7013
+ }
7014
+ yield {
7015
+ data,
7016
+ __actorRegistrationKey: registrationKey
7017
+ };
7018
+ }
7019
+ }.bind(this)
7020
+ ).then(
7021
+ (this.isCadenzaDBReady ? CadenzaService.createCadenzaDBInsertTask(
7022
+ "actor",
7023
+ {
7024
+ onConflict: {
7025
+ target: ["name", "service_name", "version"],
7026
+ action: {
7027
+ do: "nothing"
7028
+ }
7029
+ }
7030
+ },
7031
+ { concurrency: 30 }
7032
+ ) : CadenzaService.get("dbInsertActor"))?.then(
7033
+ CadenzaService.createMetaTask("Record actor registration", (ctx) => {
7034
+ if (!ctx.__syncing) {
7035
+ return;
7036
+ }
7037
+ CadenzaService.debounce("meta.sync_controller.synced_resource", {
7038
+ delayMs: 3e3
7039
+ });
7040
+ this.registeredActors.add(ctx.__actorRegistrationKey);
7041
+ return true;
7042
+ }).then(
7043
+ CadenzaService.createUniqueMetaTask(
7044
+ "Gather actor registration",
7045
+ () => true
7046
+ ).emits("meta.sync_controller.synced_actors")
7047
+ )
7048
+ )
7049
+ );
7050
+ this.registerActorTaskMapTask = CadenzaService.createMetaTask(
7051
+ "Split actor task maps",
7052
+ function* (ctx) {
7053
+ const task = ctx.task;
7054
+ if (task.hidden || !task.register) {
7055
+ return;
7056
+ }
7057
+ const metadata = getActorTaskRuntimeMetadata(task.taskFunction);
7058
+ if (!metadata?.actorName) {
7059
+ return;
7060
+ }
7061
+ const registrationKey = `${metadata.actorName}|${task.name}|${task.version}|${CadenzaService.serviceRegistry.serviceName}`;
7062
+ if (this.registeredActorTaskMaps.has(registrationKey)) {
7063
+ return;
7064
+ }
7065
+ yield {
7066
+ data: {
7067
+ actor_name: metadata.actorName,
7068
+ actor_version: 1,
7069
+ task_name: task.name,
7070
+ task_version: task.version,
7071
+ service_name: CadenzaService.serviceRegistry.serviceName,
7072
+ mode: metadata.mode,
7073
+ description: task.description ?? metadata.actorDescription ?? "",
7074
+ is_meta: metadata.actorKind === "meta" || task.isMeta === true
7075
+ },
7076
+ __actorTaskMapRegistrationKey: registrationKey
7077
+ };
7078
+ }.bind(this)
7079
+ ).then(
7080
+ (this.isCadenzaDBReady ? CadenzaService.createCadenzaDBInsertTask(
7081
+ "actor_task_map",
7082
+ {
7083
+ onConflict: {
7084
+ target: [
7085
+ "actor_name",
7086
+ "actor_version",
7087
+ "task_name",
7088
+ "task_version",
7089
+ "service_name"
7090
+ ],
7091
+ action: {
7092
+ do: "nothing"
7093
+ }
7094
+ }
7095
+ },
7096
+ { concurrency: 30 }
7097
+ ) : CadenzaService.get("dbInsertActorTaskMap"))?.then(
7098
+ CadenzaService.createMetaTask("Record actor task map registration", (ctx) => {
7099
+ if (!ctx.__syncing) {
7100
+ return;
7101
+ }
7102
+ CadenzaService.debounce("meta.sync_controller.synced_resource", {
7103
+ delayMs: 3e3
7104
+ });
7105
+ this.registeredActorTaskMaps.add(ctx.__actorTaskMapRegistrationKey);
7106
+ })
7107
+ )
7108
+ );
6333
7109
  const registerSignalTask = CadenzaService.createMetaTask(
6334
7110
  "Record signal registration",
6335
7111
  (ctx) => {
@@ -6571,12 +7347,19 @@ var GraphSyncController = class _GraphSyncController {
6571
7347
  ).then(this.splitSignalsTask);
6572
7348
  CadenzaService.registry.getAllTasks.clone().doOn("meta.sync_controller.synced_signals").then(this.splitTasksForRegistration);
6573
7349
  CadenzaService.registry.getAllRoutines.clone().doOn("meta.sync_controller.synced_tasks").then(this.splitRoutinesTask);
7350
+ CadenzaService.createMetaTask("Get all actors", (ctx) => {
7351
+ return {
7352
+ ...ctx,
7353
+ actors: CadenzaService.getAllActors()
7354
+ };
7355
+ }).doOn("meta.sync_controller.synced_tasks").then(this.splitActorsForRegistration);
6574
7356
  CadenzaService.registry.doForEachTask.clone().doOn("meta.sync_controller.synced_tasks").then(
6575
7357
  this.registerTaskMapTask,
6576
7358
  this.registerSignalToTaskMapTask,
6577
7359
  this.registerIntentToTaskMapTask,
6578
7360
  this.registerDeputyRelationshipTask
6579
7361
  );
7362
+ CadenzaService.registry.doForEachTask.clone().doOn("meta.sync_controller.synced_tasks", "meta.sync_controller.synced_actors").then(this.registerActorTaskMapTask);
6580
7363
  CadenzaService.registry.getAllRoutines.clone().doOn("meta.sync_controller.synced_routines").then(this.splitTasksInRoutines);
6581
7364
  CadenzaService.createMetaTask("Finish sync", (ctx, emit) => {
6582
7365
  emit("global.meta.sync_controller.synced", {
@@ -6963,6 +7746,14 @@ var CadenzaService = class {
6963
7746
  static get(taskName) {
6964
7747
  return Cadenza.get(taskName);
6965
7748
  }
7749
+ static getActor(actorName) {
7750
+ const cadenzaWithActors = Cadenza;
7751
+ return cadenzaWithActors.getActor?.(actorName);
7752
+ }
7753
+ static getAllActors() {
7754
+ const cadenzaWithActors = Cadenza;
7755
+ return cadenzaWithActors.getAllActors?.() ?? [];
7756
+ }
6966
7757
  static getRoutine(routineName) {
6967
7758
  return Cadenza.getRoutine(routineName);
6968
7759
  }
@@ -7528,6 +8319,14 @@ var CadenzaService = class {
7528
8319
  options.isMeta = true;
7529
8320
  this.createDatabaseService(name, schema, description, options);
7530
8321
  }
8322
+ static createActor(spec, options = {}) {
8323
+ this.bootstrap();
8324
+ return Cadenza.createActor(spec, options);
8325
+ }
8326
+ static createActorFromDefinition(definition, options = {}) {
8327
+ this.bootstrap();
8328
+ return Cadenza.createActorFromDefinition(definition, options);
8329
+ }
7531
8330
  /**
7532
8331
  * Creates and registers a new task with the provided name, function, and optional details.
7533
8332
  *
@@ -7942,6 +8741,7 @@ CadenzaService.warnedInvalidMetaIntentResponderKeys = /* @__PURE__ */ new Set();
7942
8741
 
7943
8742
  // src/index.ts
7944
8743
  import {
8744
+ Actor as Actor2,
7945
8745
  DebounceTask as DebounceTask2,
7946
8746
  EphemeralTask as EphemeralTask2,
7947
8747
  GraphRoutine as GraphRoutine2,
@@ -7949,6 +8749,7 @@ import {
7949
8749
  } from "@cadenza.io/core";
7950
8750
  var index_default = CadenzaService;
7951
8751
  export {
8752
+ Actor2 as Actor,
7952
8753
  DatabaseTask,
7953
8754
  DebounceTask2 as DebounceTask,
7954
8755
  DeputyTask,