@naylence/runtime 0.3.5-test.954 → 0.3.5-test.956
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/browser/index.cjs +115 -24
- package/dist/browser/index.mjs +115 -24
- package/dist/cjs/naylence/fame/channel/flow-controller.js +38 -1
- package/dist/cjs/naylence/fame/connector/base-async-connector.js +6 -4
- package/dist/cjs/naylence/fame/connector/broadcast-channel-connector.browser.js +54 -3
- package/dist/cjs/naylence/fame/connector/broadcast-channel-listener.js +2 -0
- package/dist/cjs/version.js +2 -2
- package/dist/esm/naylence/fame/channel/flow-controller.js +38 -1
- package/dist/esm/naylence/fame/connector/base-async-connector.js +6 -4
- package/dist/esm/naylence/fame/connector/broadcast-channel-connector.browser.js +54 -3
- package/dist/esm/naylence/fame/connector/broadcast-channel-listener.js +2 -0
- package/dist/esm/version.js +2 -2
- package/dist/node/index.cjs +115 -24
- package/dist/node/index.mjs +115 -24
- package/dist/node/node.cjs +115 -24
- package/dist/node/node.mjs +115 -24
- package/dist/types/naylence/fame/connector/broadcast-channel-connector.browser.d.ts +4 -0
- package/dist/types/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/browser/index.mjs
CHANGED
|
@@ -96,12 +96,12 @@ installProcessEnvShim();
|
|
|
96
96
|
// --- END ENV SHIM ---
|
|
97
97
|
|
|
98
98
|
// This file is auto-generated during build - do not edit manually
|
|
99
|
-
// Generated from package.json version: 0.3.5-test.
|
|
99
|
+
// Generated from package.json version: 0.3.5-test.956
|
|
100
100
|
/**
|
|
101
101
|
* The package version, injected at build time.
|
|
102
102
|
* @internal
|
|
103
103
|
*/
|
|
104
|
-
const VERSION = '0.3.5-test.
|
|
104
|
+
const VERSION = '0.3.5-test.956';
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
107
|
* Fame protocol specific error classes with WebSocket close codes and proper inheritance.
|
|
@@ -919,7 +919,7 @@ class TaskCancelledError extends Error {
|
|
|
919
919
|
* Provides functionality similar to Python's asyncio TaskSpawner with proper
|
|
920
920
|
* error handling, cancellation, and graceful shutdown capabilities.
|
|
921
921
|
*/
|
|
922
|
-
const logger$
|
|
922
|
+
const logger$1c = getLogger('naylence.fame.util.task_spawner');
|
|
923
923
|
function firstDefined(source, keys) {
|
|
924
924
|
for (const key of keys) {
|
|
925
925
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
@@ -1080,7 +1080,7 @@ class TaskSpawner {
|
|
|
1080
1080
|
const taskId = `task-${++this._taskCounter}`;
|
|
1081
1081
|
const taskName = normalizedOptions.name || `unnamed-${taskId}`;
|
|
1082
1082
|
const timeout = normalizedOptions.timeout ?? this._config.defaultTimeout;
|
|
1083
|
-
logger$
|
|
1083
|
+
logger$1c.debug('starting_background_task', {
|
|
1084
1084
|
task_name: taskName,
|
|
1085
1085
|
task_id: taskId,
|
|
1086
1086
|
});
|
|
@@ -1097,7 +1097,7 @@ class TaskSpawner {
|
|
|
1097
1097
|
task.promise
|
|
1098
1098
|
.then(() => {
|
|
1099
1099
|
if (!this._suppressCompletionLogging) {
|
|
1100
|
-
logger$
|
|
1100
|
+
logger$1c.debug('task_completed_successfully', {
|
|
1101
1101
|
task_name: taskName,
|
|
1102
1102
|
task_id: taskId,
|
|
1103
1103
|
duration_ms: Date.now() - task.startTime,
|
|
@@ -1151,7 +1151,7 @@ class TaskSpawner {
|
|
|
1151
1151
|
error.name === 'AbortError' ||
|
|
1152
1152
|
error.message === 'Task cancelled' ||
|
|
1153
1153
|
error.message === 'Aborted') {
|
|
1154
|
-
logger$
|
|
1154
|
+
logger$1c.debug('task_cancelled', {
|
|
1155
1155
|
task_name: taskName,
|
|
1156
1156
|
note: 'Task cancelled as requested',
|
|
1157
1157
|
});
|
|
@@ -1159,7 +1159,7 @@ class TaskSpawner {
|
|
|
1159
1159
|
}
|
|
1160
1160
|
// Handle timeout
|
|
1161
1161
|
if (error instanceof TaskTimeoutError) {
|
|
1162
|
-
logger$
|
|
1162
|
+
logger$1c.warning('task_timed_out', {
|
|
1163
1163
|
task_name: taskName,
|
|
1164
1164
|
error: error.message,
|
|
1165
1165
|
});
|
|
@@ -1171,7 +1171,7 @@ class TaskSpawner {
|
|
|
1171
1171
|
// Handle known WebSocket shutdown race condition (similar to Python version)
|
|
1172
1172
|
if (error.message.includes("await wasn't used with future") ||
|
|
1173
1173
|
error.message.includes('WebSocket closed during receive')) {
|
|
1174
|
-
logger$
|
|
1174
|
+
logger$1c.debug('task_shutdown_race_condition_handled', {
|
|
1175
1175
|
task_name: taskName,
|
|
1176
1176
|
note: 'Normal WebSocket close timing during shutdown - not an error',
|
|
1177
1177
|
});
|
|
@@ -1181,7 +1181,7 @@ class TaskSpawner {
|
|
|
1181
1181
|
if (error.name === 'FameTransportClose' ||
|
|
1182
1182
|
error.message.includes('normal closure') ||
|
|
1183
1183
|
error.message.includes('Connection closed')) {
|
|
1184
|
-
logger$
|
|
1184
|
+
logger$1c.debug('task_shutdown_completed_normally', {
|
|
1185
1185
|
task_name: taskName,
|
|
1186
1186
|
note: 'Task closed normally during shutdown',
|
|
1187
1187
|
});
|
|
@@ -1194,14 +1194,14 @@ class TaskSpawner {
|
|
|
1194
1194
|
// Log retriable errors as warnings (they'll be retried by upstream logic)
|
|
1195
1195
|
// Log non-retriable errors as errors (fatal failures)
|
|
1196
1196
|
if (isRetriableError) {
|
|
1197
|
-
logger$
|
|
1197
|
+
logger$1c.warning('background_task_failed', {
|
|
1198
1198
|
task_name: taskName,
|
|
1199
1199
|
error: error.message,
|
|
1200
1200
|
retriable: true,
|
|
1201
1201
|
});
|
|
1202
1202
|
}
|
|
1203
1203
|
else {
|
|
1204
|
-
logger$
|
|
1204
|
+
logger$1c.error('background_task_failed', {
|
|
1205
1205
|
task_name: taskName,
|
|
1206
1206
|
error: error.message,
|
|
1207
1207
|
stack: error.stack,
|
|
@@ -1220,11 +1220,11 @@ class TaskSpawner {
|
|
|
1220
1220
|
async shutdownTasks(options = {}) {
|
|
1221
1221
|
const { gracePeriod, cancelHanging, joinTimeout } = normalizeShutdownOptions(options);
|
|
1222
1222
|
if (this._tasks.size === 0) {
|
|
1223
|
-
logger$
|
|
1223
|
+
logger$1c.debug('shutdown_tasks_no_tasks_to_shutdown');
|
|
1224
1224
|
return;
|
|
1225
1225
|
}
|
|
1226
1226
|
this._suppressCompletionLogging = true;
|
|
1227
|
-
logger$
|
|
1227
|
+
logger$1c.debug('shutting_down_tasks', {
|
|
1228
1228
|
task_count: this._tasks.size,
|
|
1229
1229
|
task_names: Array.from(this._tasks.values()).map((t) => t.name),
|
|
1230
1230
|
grace_period_ms: gracePeriod,
|
|
@@ -1239,7 +1239,7 @@ class TaskSpawner {
|
|
|
1239
1239
|
if (cancelHanging) {
|
|
1240
1240
|
const stillRunning = tasks.filter((task) => task.getState() === TaskState.RUNNING && !completed.has(task));
|
|
1241
1241
|
if (stillRunning.length > 0) {
|
|
1242
|
-
logger$
|
|
1242
|
+
logger$1c.debug('tasks_did_not_complete_within_grace_period', {
|
|
1243
1243
|
hanging_count: stillRunning.length,
|
|
1244
1244
|
});
|
|
1245
1245
|
// Wait for them to finish with individual timeouts
|
|
@@ -1249,7 +1249,7 @@ class TaskSpawner {
|
|
|
1249
1249
|
}
|
|
1250
1250
|
catch (error) {
|
|
1251
1251
|
if (error instanceof TaskTimeoutError) {
|
|
1252
|
-
logger$
|
|
1252
|
+
logger$1c.warning('task_did_not_shutdown', {
|
|
1253
1253
|
task_name: task.name || task.id,
|
|
1254
1254
|
join_timeout_ms: joinTimeout,
|
|
1255
1255
|
});
|
|
@@ -1260,7 +1260,7 @@ class TaskSpawner {
|
|
|
1260
1260
|
}
|
|
1261
1261
|
else if (!(error instanceof TaskCancelledError)) {
|
|
1262
1262
|
/* istanbul ignore next - unreachable defensive branch */
|
|
1263
|
-
logger$
|
|
1263
|
+
logger$1c.error('task_raised_during_cancellation', {
|
|
1264
1264
|
task_name: task.name || task.id,
|
|
1265
1265
|
error: error instanceof Error ? error.message : String(error),
|
|
1266
1266
|
});
|
|
@@ -2353,6 +2353,7 @@ function validateKeyCorrelationTtlSec(ttlSec) {
|
|
|
2353
2353
|
* condition/promise and ensure at most one notifier coroutine exists for a
|
|
2354
2354
|
* flow at any time.
|
|
2355
2355
|
*/
|
|
2356
|
+
const logger$1b = getLogger('naylence.fame.flow.flow_controller');
|
|
2356
2357
|
/**
|
|
2357
2358
|
* Simple condition variable implementation for TypeScript/Node.js
|
|
2358
2359
|
* Similar to Python's asyncio.Condition
|
|
@@ -2478,8 +2479,17 @@ class FlowController {
|
|
|
2478
2479
|
// clamp into [0, initialWindow]
|
|
2479
2480
|
const newBalance = Math.max(0, Math.min(this.initialWindow, prev + delta));
|
|
2480
2481
|
this.credits.set(flowId, newBalance);
|
|
2482
|
+
const crossedZero = prev <= 0 && newBalance > 0;
|
|
2483
|
+
logger$1b.debug('flow_controller_add_credits', {
|
|
2484
|
+
flow_id: flowId,
|
|
2485
|
+
delta,
|
|
2486
|
+
prev_balance: prev,
|
|
2487
|
+
new_balance: newBalance,
|
|
2488
|
+
initial_window: this.initialWindow,
|
|
2489
|
+
crossed_zero: crossedZero,
|
|
2490
|
+
});
|
|
2481
2491
|
// wake waiters only if we crossed the zero boundary
|
|
2482
|
-
if (
|
|
2492
|
+
if (crossedZero) {
|
|
2483
2493
|
this.wakeWaiters(flowId);
|
|
2484
2494
|
}
|
|
2485
2495
|
return newBalance;
|
|
@@ -2490,11 +2500,27 @@ class FlowController {
|
|
|
2490
2500
|
async acquire(flowId) {
|
|
2491
2501
|
this.ensureFlow(flowId);
|
|
2492
2502
|
const condition = this.conditions.get(flowId);
|
|
2503
|
+
logger$1b.debug('flow_controller_acquire_attempt', {
|
|
2504
|
+
flow_id: flowId,
|
|
2505
|
+
current_balance: this.credits.get(flowId),
|
|
2506
|
+
});
|
|
2493
2507
|
while (this.credits.get(flowId) <= 0) {
|
|
2508
|
+
logger$1b.debug('flow_controller_waiting_for_credit', {
|
|
2509
|
+
flow_id: flowId,
|
|
2510
|
+
});
|
|
2494
2511
|
await condition.wait();
|
|
2512
|
+
logger$1b.debug('flow_controller_woke_with_credit', {
|
|
2513
|
+
flow_id: flowId,
|
|
2514
|
+
balance_after_wake: this.credits.get(flowId),
|
|
2515
|
+
});
|
|
2495
2516
|
}
|
|
2496
2517
|
const current = this.credits.get(flowId);
|
|
2497
2518
|
this.credits.set(flowId, current - 1);
|
|
2519
|
+
logger$1b.debug('flow_controller_credit_consumed', {
|
|
2520
|
+
flow_id: flowId,
|
|
2521
|
+
prev_balance: current,
|
|
2522
|
+
remaining_balance: current - 1,
|
|
2523
|
+
});
|
|
2498
2524
|
}
|
|
2499
2525
|
/**
|
|
2500
2526
|
* Consume *credits* immediately (non-blocking).
|
|
@@ -2514,6 +2540,12 @@ class FlowController {
|
|
|
2514
2540
|
const current = this.credits.get(flowId);
|
|
2515
2541
|
const remaining = Math.max(current - credits, 0);
|
|
2516
2542
|
this.credits.set(flowId, remaining);
|
|
2543
|
+
logger$1b.debug('flow_controller_consume', {
|
|
2544
|
+
flow_id: flowId,
|
|
2545
|
+
requested: credits,
|
|
2546
|
+
prev_balance: current,
|
|
2547
|
+
remaining_balance: remaining,
|
|
2548
|
+
});
|
|
2517
2549
|
return remaining;
|
|
2518
2550
|
}
|
|
2519
2551
|
/**
|
|
@@ -2534,6 +2566,10 @@ class FlowController {
|
|
|
2534
2566
|
this.windowIds.delete(flowId);
|
|
2535
2567
|
this.credits.set(flowId, this.initialWindow);
|
|
2536
2568
|
this.wakeWaiters(flowId);
|
|
2569
|
+
logger$1b.debug('flow_controller_flow_reset', {
|
|
2570
|
+
flow_id: flowId,
|
|
2571
|
+
reset_balance: this.initialWindow,
|
|
2572
|
+
});
|
|
2537
2573
|
}
|
|
2538
2574
|
/**
|
|
2539
2575
|
* Return `[windowId, flags]` for the next outbound envelope.
|
|
@@ -9381,10 +9417,12 @@ class BaseAsyncConnector extends TaskSpawner {
|
|
|
9381
9417
|
throw new FameTransportClose('Connection closed', 1006);
|
|
9382
9418
|
}
|
|
9383
9419
|
// Apply flow control if enabled and not a credit update
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
'credits' in envelope.frame)
|
|
9420
|
+
const isCreditUpdateFrame = Boolean(envelope.frame &&
|
|
9421
|
+
(envelope.frame.type === 'CreditUpdate' ||
|
|
9422
|
+
envelope.frame.type === 'credit_update' ||
|
|
9423
|
+
('flowId' in envelope.frame && 'credits' in envelope.frame) ||
|
|
9424
|
+
('flow_id' in envelope.frame && 'credits' in envelope.frame)));
|
|
9425
|
+
if (this._fcEnabled && !isCreditUpdateFrame) {
|
|
9388
9426
|
const flowId = envelope.flowId || this._connectorFlowId;
|
|
9389
9427
|
envelope.flowId = flowId;
|
|
9390
9428
|
const t0 = this._metrics ? performance.now() : 0;
|
|
@@ -9879,6 +9917,7 @@ let BroadcastChannelConnector$2 = class BroadcastChannelConnector extends BaseAs
|
|
|
9879
9917
|
? Math.floor(config.inboxCapacity)
|
|
9880
9918
|
: DEFAULT_INBOX_CAPACITY$7;
|
|
9881
9919
|
this.inbox = new BoundedAsyncQueue(preferredCapacity);
|
|
9920
|
+
this.inboxCapacity = preferredCapacity;
|
|
9882
9921
|
this.connectorId = BroadcastChannelConnector.generateConnectorId();
|
|
9883
9922
|
this.channel = new BroadcastChannel(this.channelName);
|
|
9884
9923
|
logger$_.debug('broadcast_channel_connector_created', {
|
|
@@ -9938,15 +9977,27 @@ let BroadcastChannelConnector$2 = class BroadcastChannelConnector extends BaseAs
|
|
|
9938
9977
|
if (typeof this.inbox.tryEnqueue === 'function') {
|
|
9939
9978
|
const accepted = this.inbox.tryEnqueue(payload);
|
|
9940
9979
|
if (accepted) {
|
|
9980
|
+
this.logInboxSnapshot('broadcast_channel_inbox_enqueued', {
|
|
9981
|
+
source: 'listener',
|
|
9982
|
+
enqueue_strategy: 'try',
|
|
9983
|
+
payload_length: payload.byteLength,
|
|
9984
|
+
});
|
|
9941
9985
|
return;
|
|
9942
9986
|
}
|
|
9943
9987
|
}
|
|
9944
9988
|
this.inbox.enqueue(payload);
|
|
9989
|
+
this.logInboxSnapshot('broadcast_channel_inbox_enqueued', {
|
|
9990
|
+
source: 'listener',
|
|
9991
|
+
enqueue_strategy: 'enqueue',
|
|
9992
|
+
payload_length: payload.byteLength,
|
|
9993
|
+
});
|
|
9945
9994
|
}
|
|
9946
9995
|
catch (error) {
|
|
9947
9996
|
if (error instanceof QueueFullError) {
|
|
9948
9997
|
logger$_.warning('broadcast_channel_receive_queue_full', {
|
|
9949
9998
|
channel: this.channelName,
|
|
9999
|
+
inbox_capacity: this.inboxCapacity,
|
|
10000
|
+
inbox_remaining_capacity: this.inbox.remainingCapacity,
|
|
9950
10001
|
});
|
|
9951
10002
|
}
|
|
9952
10003
|
else {
|
|
@@ -9957,8 +10008,10 @@ let BroadcastChannelConnector$2 = class BroadcastChannelConnector extends BaseAs
|
|
|
9957
10008
|
}
|
|
9958
10009
|
}
|
|
9959
10010
|
};
|
|
9960
|
-
|
|
9961
|
-
|
|
10011
|
+
if (!config.passive) {
|
|
10012
|
+
this.channel.addEventListener('message', this.onMsg);
|
|
10013
|
+
this.listenerRegistered = true;
|
|
10014
|
+
}
|
|
9962
10015
|
// Setup visibility change monitoring
|
|
9963
10016
|
this.visibilityChangeHandler = () => {
|
|
9964
10017
|
const isHidden = document.hidden;
|
|
@@ -10030,15 +10083,25 @@ let BroadcastChannelConnector$2 = class BroadcastChannelConnector extends BaseAs
|
|
|
10030
10083
|
if (typeof this.inbox.tryEnqueue === 'function') {
|
|
10031
10084
|
const accepted = this.inbox.tryEnqueue(item);
|
|
10032
10085
|
if (accepted) {
|
|
10086
|
+
this.logInboxSnapshot('broadcast_channel_push_enqueued', {
|
|
10087
|
+
enqueue_strategy: 'try',
|
|
10088
|
+
item_type: this._describeInboxItem(item),
|
|
10089
|
+
});
|
|
10033
10090
|
return;
|
|
10034
10091
|
}
|
|
10035
10092
|
}
|
|
10036
10093
|
this.inbox.enqueue(item);
|
|
10094
|
+
this.logInboxSnapshot('broadcast_channel_push_enqueued', {
|
|
10095
|
+
enqueue_strategy: 'enqueue',
|
|
10096
|
+
item_type: this._describeInboxItem(item),
|
|
10097
|
+
});
|
|
10037
10098
|
}
|
|
10038
10099
|
catch (error) {
|
|
10039
10100
|
if (error instanceof QueueFullError) {
|
|
10040
10101
|
logger$_.warning('broadcast_channel_push_queue_full', {
|
|
10041
10102
|
channel: this.channelName,
|
|
10103
|
+
inbox_capacity: this.inboxCapacity,
|
|
10104
|
+
inbox_remaining_capacity: this.inbox.remainingCapacity,
|
|
10042
10105
|
});
|
|
10043
10106
|
throw error;
|
|
10044
10107
|
}
|
|
@@ -10061,7 +10124,11 @@ let BroadcastChannelConnector$2 = class BroadcastChannelConnector extends BaseAs
|
|
|
10061
10124
|
});
|
|
10062
10125
|
}
|
|
10063
10126
|
async _transportReceive() {
|
|
10064
|
-
|
|
10127
|
+
const item = await this.inbox.dequeue();
|
|
10128
|
+
this.logInboxSnapshot('broadcast_channel_inbox_dequeued', {
|
|
10129
|
+
item_type: this._describeInboxItem(item),
|
|
10130
|
+
});
|
|
10131
|
+
return item;
|
|
10065
10132
|
}
|
|
10066
10133
|
async _transportClose(code, reason) {
|
|
10067
10134
|
logger$_.debug('broadcast_channel_transport_closing', {
|
|
@@ -10115,6 +10182,28 @@ let BroadcastChannelConnector$2 = class BroadcastChannelConnector extends BaseAs
|
|
|
10115
10182
|
}
|
|
10116
10183
|
return rawOrEnvelope;
|
|
10117
10184
|
}
|
|
10185
|
+
_describeInboxItem(item) {
|
|
10186
|
+
if (item instanceof Uint8Array) {
|
|
10187
|
+
return 'bytes';
|
|
10188
|
+
}
|
|
10189
|
+
if (item.envelope) {
|
|
10190
|
+
return 'channel_message';
|
|
10191
|
+
}
|
|
10192
|
+
if (item.frame) {
|
|
10193
|
+
return 'envelope';
|
|
10194
|
+
}
|
|
10195
|
+
return 'unknown';
|
|
10196
|
+
}
|
|
10197
|
+
logInboxSnapshot(event, extra = {}) {
|
|
10198
|
+
logger$_.debug(event, {
|
|
10199
|
+
channel: this.channelName,
|
|
10200
|
+
connector_id: this.connectorId,
|
|
10201
|
+
connector_state: this.state,
|
|
10202
|
+
inbox_capacity: this.inboxCapacity,
|
|
10203
|
+
inbox_remaining_capacity: this.inbox.remainingCapacity,
|
|
10204
|
+
...extra,
|
|
10205
|
+
});
|
|
10206
|
+
}
|
|
10118
10207
|
_shouldSkipDuplicateAck(senderId, payload) {
|
|
10119
10208
|
const dedupKey = this._extractAckDedupKey(payload);
|
|
10120
10209
|
if (!dedupKey) {
|
|
@@ -29941,6 +30030,7 @@ class BroadcastChannelListener extends TransportListener {
|
|
|
29941
30030
|
type: BROADCAST_CHANNEL_CONNECTOR_TYPE,
|
|
29942
30031
|
channelName: this._channelName,
|
|
29943
30032
|
inboxCapacity: this._inboxCapacity,
|
|
30033
|
+
passive: true,
|
|
29944
30034
|
};
|
|
29945
30035
|
}
|
|
29946
30036
|
try {
|
|
@@ -30001,6 +30091,7 @@ class BroadcastChannelListener extends TransportListener {
|
|
|
30001
30091
|
type: BROADCAST_CHANNEL_CONNECTOR_TYPE,
|
|
30002
30092
|
channelName: this._channelName,
|
|
30003
30093
|
inboxCapacity: this._inboxCapacity,
|
|
30094
|
+
passive: true,
|
|
30004
30095
|
};
|
|
30005
30096
|
const channelCandidate = candidate.channelName ?? candidate['channel_name'];
|
|
30006
30097
|
if (typeof channelCandidate === 'string' && channelCandidate.trim().length > 0) {
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
26
|
exports.FlowController = void 0;
|
|
27
27
|
const core_1 = require("@naylence/core");
|
|
28
|
+
const logging_js_1 = require("../util/logging.js");
|
|
29
|
+
const logger = (0, logging_js_1.getLogger)('naylence.fame.flow.flow_controller');
|
|
28
30
|
/**
|
|
29
31
|
* Simple condition variable implementation for TypeScript/Node.js
|
|
30
32
|
* Similar to Python's asyncio.Condition
|
|
@@ -150,8 +152,17 @@ class FlowController {
|
|
|
150
152
|
// clamp into [0, initialWindow]
|
|
151
153
|
const newBalance = Math.max(0, Math.min(this.initialWindow, prev + delta));
|
|
152
154
|
this.credits.set(flowId, newBalance);
|
|
155
|
+
const crossedZero = prev <= 0 && newBalance > 0;
|
|
156
|
+
logger.debug('flow_controller_add_credits', {
|
|
157
|
+
flow_id: flowId,
|
|
158
|
+
delta,
|
|
159
|
+
prev_balance: prev,
|
|
160
|
+
new_balance: newBalance,
|
|
161
|
+
initial_window: this.initialWindow,
|
|
162
|
+
crossed_zero: crossedZero,
|
|
163
|
+
});
|
|
153
164
|
// wake waiters only if we crossed the zero boundary
|
|
154
|
-
if (
|
|
165
|
+
if (crossedZero) {
|
|
155
166
|
this.wakeWaiters(flowId);
|
|
156
167
|
}
|
|
157
168
|
return newBalance;
|
|
@@ -162,11 +173,27 @@ class FlowController {
|
|
|
162
173
|
async acquire(flowId) {
|
|
163
174
|
this.ensureFlow(flowId);
|
|
164
175
|
const condition = this.conditions.get(flowId);
|
|
176
|
+
logger.debug('flow_controller_acquire_attempt', {
|
|
177
|
+
flow_id: flowId,
|
|
178
|
+
current_balance: this.credits.get(flowId),
|
|
179
|
+
});
|
|
165
180
|
while (this.credits.get(flowId) <= 0) {
|
|
181
|
+
logger.debug('flow_controller_waiting_for_credit', {
|
|
182
|
+
flow_id: flowId,
|
|
183
|
+
});
|
|
166
184
|
await condition.wait();
|
|
185
|
+
logger.debug('flow_controller_woke_with_credit', {
|
|
186
|
+
flow_id: flowId,
|
|
187
|
+
balance_after_wake: this.credits.get(flowId),
|
|
188
|
+
});
|
|
167
189
|
}
|
|
168
190
|
const current = this.credits.get(flowId);
|
|
169
191
|
this.credits.set(flowId, current - 1);
|
|
192
|
+
logger.debug('flow_controller_credit_consumed', {
|
|
193
|
+
flow_id: flowId,
|
|
194
|
+
prev_balance: current,
|
|
195
|
+
remaining_balance: current - 1,
|
|
196
|
+
});
|
|
170
197
|
}
|
|
171
198
|
/**
|
|
172
199
|
* Consume *credits* immediately (non-blocking).
|
|
@@ -186,6 +213,12 @@ class FlowController {
|
|
|
186
213
|
const current = this.credits.get(flowId);
|
|
187
214
|
const remaining = Math.max(current - credits, 0);
|
|
188
215
|
this.credits.set(flowId, remaining);
|
|
216
|
+
logger.debug('flow_controller_consume', {
|
|
217
|
+
flow_id: flowId,
|
|
218
|
+
requested: credits,
|
|
219
|
+
prev_balance: current,
|
|
220
|
+
remaining_balance: remaining,
|
|
221
|
+
});
|
|
189
222
|
return remaining;
|
|
190
223
|
}
|
|
191
224
|
/**
|
|
@@ -206,6 +239,10 @@ class FlowController {
|
|
|
206
239
|
this.windowIds.delete(flowId);
|
|
207
240
|
this.credits.set(flowId, this.initialWindow);
|
|
208
241
|
this.wakeWaiters(flowId);
|
|
242
|
+
logger.debug('flow_controller_flow_reset', {
|
|
243
|
+
flow_id: flowId,
|
|
244
|
+
reset_balance: this.initialWindow,
|
|
245
|
+
});
|
|
209
246
|
}
|
|
210
247
|
/**
|
|
211
248
|
* Return `[windowId, flags]` for the next outbound envelope.
|
|
@@ -261,10 +261,12 @@ class BaseAsyncConnector extends task_spawner_js_1.TaskSpawner {
|
|
|
261
261
|
throw new errors_js_1.FameTransportClose('Connection closed', 1006);
|
|
262
262
|
}
|
|
263
263
|
// Apply flow control if enabled and not a credit update
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
'credits' in envelope.frame)
|
|
264
|
+
const isCreditUpdateFrame = Boolean(envelope.frame &&
|
|
265
|
+
(envelope.frame.type === 'CreditUpdate' ||
|
|
266
|
+
envelope.frame.type === 'credit_update' ||
|
|
267
|
+
('flowId' in envelope.frame && 'credits' in envelope.frame) ||
|
|
268
|
+
('flow_id' in envelope.frame && 'credits' in envelope.frame)));
|
|
269
|
+
if (this._fcEnabled && !isCreditUpdateFrame) {
|
|
268
270
|
const flowId = envelope.flowId || this._connectorFlowId;
|
|
269
271
|
envelope.flowId = flowId;
|
|
270
272
|
const t0 = this._metrics ? performance.now() : 0;
|
|
@@ -69,6 +69,7 @@ class BroadcastChannelConnector extends base_async_connector_js_1.BaseAsyncConne
|
|
|
69
69
|
? Math.floor(config.inboxCapacity)
|
|
70
70
|
: DEFAULT_INBOX_CAPACITY;
|
|
71
71
|
this.inbox = new bounded_async_queue_js_1.BoundedAsyncQueue(preferredCapacity);
|
|
72
|
+
this.inboxCapacity = preferredCapacity;
|
|
72
73
|
this.connectorId = BroadcastChannelConnector.generateConnectorId();
|
|
73
74
|
this.channel = new BroadcastChannel(this.channelName);
|
|
74
75
|
logger.debug('broadcast_channel_connector_created', {
|
|
@@ -128,15 +129,27 @@ class BroadcastChannelConnector extends base_async_connector_js_1.BaseAsyncConne
|
|
|
128
129
|
if (typeof this.inbox.tryEnqueue === 'function') {
|
|
129
130
|
const accepted = this.inbox.tryEnqueue(payload);
|
|
130
131
|
if (accepted) {
|
|
132
|
+
this.logInboxSnapshot('broadcast_channel_inbox_enqueued', {
|
|
133
|
+
source: 'listener',
|
|
134
|
+
enqueue_strategy: 'try',
|
|
135
|
+
payload_length: payload.byteLength,
|
|
136
|
+
});
|
|
131
137
|
return;
|
|
132
138
|
}
|
|
133
139
|
}
|
|
134
140
|
this.inbox.enqueue(payload);
|
|
141
|
+
this.logInboxSnapshot('broadcast_channel_inbox_enqueued', {
|
|
142
|
+
source: 'listener',
|
|
143
|
+
enqueue_strategy: 'enqueue',
|
|
144
|
+
payload_length: payload.byteLength,
|
|
145
|
+
});
|
|
135
146
|
}
|
|
136
147
|
catch (error) {
|
|
137
148
|
if (error instanceof bounded_async_queue_js_1.QueueFullError) {
|
|
138
149
|
logger.warning('broadcast_channel_receive_queue_full', {
|
|
139
150
|
channel: this.channelName,
|
|
151
|
+
inbox_capacity: this.inboxCapacity,
|
|
152
|
+
inbox_remaining_capacity: this.inbox.remainingCapacity,
|
|
140
153
|
});
|
|
141
154
|
}
|
|
142
155
|
else {
|
|
@@ -147,8 +160,10 @@ class BroadcastChannelConnector extends base_async_connector_js_1.BaseAsyncConne
|
|
|
147
160
|
}
|
|
148
161
|
}
|
|
149
162
|
};
|
|
150
|
-
|
|
151
|
-
|
|
163
|
+
if (!config.passive) {
|
|
164
|
+
this.channel.addEventListener('message', this.onMsg);
|
|
165
|
+
this.listenerRegistered = true;
|
|
166
|
+
}
|
|
152
167
|
// Setup visibility change monitoring
|
|
153
168
|
this.visibilityChangeHandler = () => {
|
|
154
169
|
const isHidden = document.hidden;
|
|
@@ -220,15 +235,25 @@ class BroadcastChannelConnector extends base_async_connector_js_1.BaseAsyncConne
|
|
|
220
235
|
if (typeof this.inbox.tryEnqueue === 'function') {
|
|
221
236
|
const accepted = this.inbox.tryEnqueue(item);
|
|
222
237
|
if (accepted) {
|
|
238
|
+
this.logInboxSnapshot('broadcast_channel_push_enqueued', {
|
|
239
|
+
enqueue_strategy: 'try',
|
|
240
|
+
item_type: this._describeInboxItem(item),
|
|
241
|
+
});
|
|
223
242
|
return;
|
|
224
243
|
}
|
|
225
244
|
}
|
|
226
245
|
this.inbox.enqueue(item);
|
|
246
|
+
this.logInboxSnapshot('broadcast_channel_push_enqueued', {
|
|
247
|
+
enqueue_strategy: 'enqueue',
|
|
248
|
+
item_type: this._describeInboxItem(item),
|
|
249
|
+
});
|
|
227
250
|
}
|
|
228
251
|
catch (error) {
|
|
229
252
|
if (error instanceof bounded_async_queue_js_1.QueueFullError) {
|
|
230
253
|
logger.warning('broadcast_channel_push_queue_full', {
|
|
231
254
|
channel: this.channelName,
|
|
255
|
+
inbox_capacity: this.inboxCapacity,
|
|
256
|
+
inbox_remaining_capacity: this.inbox.remainingCapacity,
|
|
232
257
|
});
|
|
233
258
|
throw error;
|
|
234
259
|
}
|
|
@@ -251,7 +276,11 @@ class BroadcastChannelConnector extends base_async_connector_js_1.BaseAsyncConne
|
|
|
251
276
|
});
|
|
252
277
|
}
|
|
253
278
|
async _transportReceive() {
|
|
254
|
-
|
|
279
|
+
const item = await this.inbox.dequeue();
|
|
280
|
+
this.logInboxSnapshot('broadcast_channel_inbox_dequeued', {
|
|
281
|
+
item_type: this._describeInboxItem(item),
|
|
282
|
+
});
|
|
283
|
+
return item;
|
|
255
284
|
}
|
|
256
285
|
async _transportClose(code, reason) {
|
|
257
286
|
logger.debug('broadcast_channel_transport_closing', {
|
|
@@ -305,6 +334,28 @@ class BroadcastChannelConnector extends base_async_connector_js_1.BaseAsyncConne
|
|
|
305
334
|
}
|
|
306
335
|
return rawOrEnvelope;
|
|
307
336
|
}
|
|
337
|
+
_describeInboxItem(item) {
|
|
338
|
+
if (item instanceof Uint8Array) {
|
|
339
|
+
return 'bytes';
|
|
340
|
+
}
|
|
341
|
+
if (item.envelope) {
|
|
342
|
+
return 'channel_message';
|
|
343
|
+
}
|
|
344
|
+
if (item.frame) {
|
|
345
|
+
return 'envelope';
|
|
346
|
+
}
|
|
347
|
+
return 'unknown';
|
|
348
|
+
}
|
|
349
|
+
logInboxSnapshot(event, extra = {}) {
|
|
350
|
+
logger.debug(event, {
|
|
351
|
+
channel: this.channelName,
|
|
352
|
+
connector_id: this.connectorId,
|
|
353
|
+
connector_state: this.state,
|
|
354
|
+
inbox_capacity: this.inboxCapacity,
|
|
355
|
+
inbox_remaining_capacity: this.inbox.remainingCapacity,
|
|
356
|
+
...extra,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
308
359
|
_shouldSkipDuplicateAck(senderId, payload) {
|
|
309
360
|
const dedupKey = this._extractAckDedupKey(payload);
|
|
310
361
|
if (!dedupKey) {
|
|
@@ -342,6 +342,7 @@ class BroadcastChannelListener extends transport_listener_js_1.TransportListener
|
|
|
342
342
|
type: broadcast_channel_connector_js_1.BROADCAST_CHANNEL_CONNECTOR_TYPE,
|
|
343
343
|
channelName: this._channelName,
|
|
344
344
|
inboxCapacity: this._inboxCapacity,
|
|
345
|
+
passive: true,
|
|
345
346
|
};
|
|
346
347
|
}
|
|
347
348
|
try {
|
|
@@ -402,6 +403,7 @@ class BroadcastChannelListener extends transport_listener_js_1.TransportListener
|
|
|
402
403
|
type: broadcast_channel_connector_js_1.BROADCAST_CHANNEL_CONNECTOR_TYPE,
|
|
403
404
|
channelName: this._channelName,
|
|
404
405
|
inboxCapacity: this._inboxCapacity,
|
|
406
|
+
passive: true,
|
|
405
407
|
};
|
|
406
408
|
const channelCandidate = candidate.channelName ?? candidate['channel_name'];
|
|
407
409
|
if (typeof channelCandidate === 'string' && channelCandidate.trim().length > 0) {
|
package/dist/cjs/version.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// This file is auto-generated during build - do not edit manually
|
|
3
|
-
// Generated from package.json version: 0.3.5-test.
|
|
3
|
+
// Generated from package.json version: 0.3.5-test.956
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.VERSION = void 0;
|
|
6
6
|
/**
|
|
7
7
|
* The package version, injected at build time.
|
|
8
8
|
* @internal
|
|
9
9
|
*/
|
|
10
|
-
exports.VERSION = '0.3.5-test.
|
|
10
|
+
exports.VERSION = '0.3.5-test.956';
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* flow at any time.
|
|
23
23
|
*/
|
|
24
24
|
import { FlowFlags } from '@naylence/core';
|
|
25
|
+
import { getLogger } from '../util/logging.js';
|
|
26
|
+
const logger = getLogger('naylence.fame.flow.flow_controller');
|
|
25
27
|
/**
|
|
26
28
|
* Simple condition variable implementation for TypeScript/Node.js
|
|
27
29
|
* Similar to Python's asyncio.Condition
|
|
@@ -147,8 +149,17 @@ export class FlowController {
|
|
|
147
149
|
// clamp into [0, initialWindow]
|
|
148
150
|
const newBalance = Math.max(0, Math.min(this.initialWindow, prev + delta));
|
|
149
151
|
this.credits.set(flowId, newBalance);
|
|
152
|
+
const crossedZero = prev <= 0 && newBalance > 0;
|
|
153
|
+
logger.debug('flow_controller_add_credits', {
|
|
154
|
+
flow_id: flowId,
|
|
155
|
+
delta,
|
|
156
|
+
prev_balance: prev,
|
|
157
|
+
new_balance: newBalance,
|
|
158
|
+
initial_window: this.initialWindow,
|
|
159
|
+
crossed_zero: crossedZero,
|
|
160
|
+
});
|
|
150
161
|
// wake waiters only if we crossed the zero boundary
|
|
151
|
-
if (
|
|
162
|
+
if (crossedZero) {
|
|
152
163
|
this.wakeWaiters(flowId);
|
|
153
164
|
}
|
|
154
165
|
return newBalance;
|
|
@@ -159,11 +170,27 @@ export class FlowController {
|
|
|
159
170
|
async acquire(flowId) {
|
|
160
171
|
this.ensureFlow(flowId);
|
|
161
172
|
const condition = this.conditions.get(flowId);
|
|
173
|
+
logger.debug('flow_controller_acquire_attempt', {
|
|
174
|
+
flow_id: flowId,
|
|
175
|
+
current_balance: this.credits.get(flowId),
|
|
176
|
+
});
|
|
162
177
|
while (this.credits.get(flowId) <= 0) {
|
|
178
|
+
logger.debug('flow_controller_waiting_for_credit', {
|
|
179
|
+
flow_id: flowId,
|
|
180
|
+
});
|
|
163
181
|
await condition.wait();
|
|
182
|
+
logger.debug('flow_controller_woke_with_credit', {
|
|
183
|
+
flow_id: flowId,
|
|
184
|
+
balance_after_wake: this.credits.get(flowId),
|
|
185
|
+
});
|
|
164
186
|
}
|
|
165
187
|
const current = this.credits.get(flowId);
|
|
166
188
|
this.credits.set(flowId, current - 1);
|
|
189
|
+
logger.debug('flow_controller_credit_consumed', {
|
|
190
|
+
flow_id: flowId,
|
|
191
|
+
prev_balance: current,
|
|
192
|
+
remaining_balance: current - 1,
|
|
193
|
+
});
|
|
167
194
|
}
|
|
168
195
|
/**
|
|
169
196
|
* Consume *credits* immediately (non-blocking).
|
|
@@ -183,6 +210,12 @@ export class FlowController {
|
|
|
183
210
|
const current = this.credits.get(flowId);
|
|
184
211
|
const remaining = Math.max(current - credits, 0);
|
|
185
212
|
this.credits.set(flowId, remaining);
|
|
213
|
+
logger.debug('flow_controller_consume', {
|
|
214
|
+
flow_id: flowId,
|
|
215
|
+
requested: credits,
|
|
216
|
+
prev_balance: current,
|
|
217
|
+
remaining_balance: remaining,
|
|
218
|
+
});
|
|
186
219
|
return remaining;
|
|
187
220
|
}
|
|
188
221
|
/**
|
|
@@ -203,6 +236,10 @@ export class FlowController {
|
|
|
203
236
|
this.windowIds.delete(flowId);
|
|
204
237
|
this.credits.set(flowId, this.initialWindow);
|
|
205
238
|
this.wakeWaiters(flowId);
|
|
239
|
+
logger.debug('flow_controller_flow_reset', {
|
|
240
|
+
flow_id: flowId,
|
|
241
|
+
reset_balance: this.initialWindow,
|
|
242
|
+
});
|
|
206
243
|
}
|
|
207
244
|
/**
|
|
208
245
|
* Return `[windowId, flags]` for the next outbound envelope.
|