@standardagents/builder 0.41.8 → 0.41.9
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/built-in-routes.js +5 -2
- package/dist/built-in-routes.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +283 -9
- package/dist/index.js.map +1 -1
- package/dist/{resolver-BMZAzKD3.d.ts → resolver-DWuGxI4w.d.ts} +39 -4
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +283 -9
- package/dist/runtime.js.map +1 -1
- package/package.json +5 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as AuthContext, b as ThreadEnv, v as Agent, x as ThreadMetadata, z as ToolResult, M as Message, n as ClientToolRequest, o as ClientToolResponse, V as ProxyStreamReceiver, W as ProxyStreamFrame, p as ClientStreamRequest, X as ProxyStreamTarget, q as ClientStreamResponse, Y as ProxyStreamOpenResult, H as FileRecord, Z as SkillStorageScope, F as FlowState, N as FileStats, K as GrepResult, J as AttachmentRef, U as MessageContent, G as Env, w as ThreadInstance } from './index-ChHAVogf.js';
|
|
2
2
|
import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
|
|
3
|
-
import { CodeExecutionOptions, CodeExecutionResult, SubagentRegistryEntry, InjectMessageInput, QueueMessageInput, CredentialGrant, SkillMetadata, SkillFileInput, ModelDefinition as ModelDefinition$1, ToolArgs, PromptTextPart, PromptEnvPart, VariableDefinition, SubpromptConfig as SubpromptConfig$1, PromptToolConfig as PromptToolConfig$1, ReasoningConfig, HumanSide as HumanSide$1, SubagentRelationship as SubagentRelationship$1, ParentHookRequirement, LLMProviderInterface, ContentPart, TextPart, ImagePart, FilePart, ThreadState, NamespaceContext, DefinitionLoader } from '@standardagents/spec';
|
|
3
|
+
import { CodeExecutionOptions, CodeExecutionResult, SubagentRegistryEntry, InjectMessageInput, QueueMessageInput, UserRoomConnection, CredentialGrant, SkillMetadata, SkillFileInput, ModelDefinition as ModelDefinition$1, ToolArgs, PromptTextPart, PromptEnvPart, VariableDefinition, SubpromptConfig as SubpromptConfig$1, PromptToolConfig as PromptToolConfig$1, ReasoningConfig, HumanSide as HumanSide$1, SubagentRelationship as SubagentRelationship$1, ParentHookRequirement, LLMProviderInterface, ContentPart, TextPart, ImagePart, FilePart, ThreadState, NamespaceContext, DefinitionLoader } from '@standardagents/spec';
|
|
4
4
|
import { N as NamespacedRegistry } from './types-Dhd0M369.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -1892,12 +1892,20 @@ interface HubClientInfo {
|
|
|
1892
1892
|
clientId: string | null;
|
|
1893
1893
|
clientName: string | null;
|
|
1894
1894
|
clientKind: string | null;
|
|
1895
|
+
/** Unix milliseconds when the socket attached; restored from the attachment. */
|
|
1896
|
+
since?: number;
|
|
1895
1897
|
}
|
|
1896
1898
|
declare class DurableUserHub extends DurableObject<DurableUserHubEnv> {
|
|
1897
1899
|
/** Persistent client (daemon) sockets, restored across hibernation. */
|
|
1898
1900
|
private clientSockets;
|
|
1899
1901
|
private clientInfo;
|
|
1900
1902
|
private migrated;
|
|
1903
|
+
/** Inbound frame rate and outbound byte budget per room socket. */
|
|
1904
|
+
private roomLimits;
|
|
1905
|
+
/** Detach notices held for the presence grace window, by client id. */
|
|
1906
|
+
private pendingDetach;
|
|
1907
|
+
/** The project's room definition, loaded once per instance. */
|
|
1908
|
+
private roomDefinition;
|
|
1901
1909
|
constructor(ctx: DurableObjectState, env: DurableUserHubEnv);
|
|
1902
1910
|
private ensureMigrated;
|
|
1903
1911
|
/**
|
|
@@ -1920,6 +1928,32 @@ declare class DurableUserHub extends DurableObject<DurableUserHubEnv> {
|
|
|
1920
1928
|
* opened separately and only while a thread is active.
|
|
1921
1929
|
*/
|
|
1922
1930
|
fetch(request: Request): Promise<Response>;
|
|
1931
|
+
private connectionOf;
|
|
1932
|
+
/** Close every other socket presenting `clientId`; true when one existed. */
|
|
1933
|
+
private supersedeClient;
|
|
1934
|
+
private forgetSocket;
|
|
1935
|
+
private liveSocketFor;
|
|
1936
|
+
/** Live room connections, one per client id, oldest attachment first. */
|
|
1937
|
+
listRoomClients(): UserRoomConnection[];
|
|
1938
|
+
/** Deliver one frame to the socket carrying `clientId`, within its send budget. */
|
|
1939
|
+
sendToRoomClient(clientId: string, frame: unknown): boolean;
|
|
1940
|
+
private sendRoomFrame;
|
|
1941
|
+
private limitsFor;
|
|
1942
|
+
/** Load the project's room definition from the users registry, once. */
|
|
1943
|
+
private loadRoom;
|
|
1944
|
+
/** Whether `key` is room-owned state, exempt from `user_value_changed`. */
|
|
1945
|
+
private roomOwnsKey;
|
|
1946
|
+
/**
|
|
1947
|
+
* Presence runs inside the serialization chain, after any execution the
|
|
1948
|
+
* event raced with, so a room's presence write never interleaves with a
|
|
1949
|
+
* mutation. A reconnect inside the grace window is not a change.
|
|
1950
|
+
*/
|
|
1951
|
+
private observeAttach;
|
|
1952
|
+
private observeDetach;
|
|
1953
|
+
private dispatchRoomPresence;
|
|
1954
|
+
/** The hub is one object per user; its name is the user id. */
|
|
1955
|
+
private roomUserId;
|
|
1956
|
+
private dispatchRoomFrame;
|
|
1923
1957
|
/** Idle sockets answer heartbeats at the edge without waking the object.
|
|
1924
1958
|
* Registered at accept time rather than in the constructor so a hub that
|
|
1925
1959
|
* never accepts a socket stays fully evictable (this also keeps
|
|
@@ -1929,8 +1963,9 @@ declare class DurableUserHub extends DurableObject<DurableUserHubEnv> {
|
|
|
1929
1963
|
private streamSockets;
|
|
1930
1964
|
private acceptUserStream;
|
|
1931
1965
|
/**
|
|
1932
|
-
* Deliver an event frame to every socket attached to this user's stream
|
|
1933
|
-
* At-most-once, live-only (spec: Users §4) —
|
|
1966
|
+
* Deliver an event frame to every socket attached to this user's stream
|
|
1967
|
+
* and to every room socket. At-most-once, live-only (spec: Users §4) —
|
|
1968
|
+
* no history, no acks.
|
|
1934
1969
|
*/
|
|
1935
1970
|
broadcastUserEvent(userId: string, event: string, data?: unknown): void;
|
|
1936
1971
|
/** Per-user execution chain (spec: Users §2.1) — see users/serialize.ts. */
|
|
@@ -1959,7 +1994,7 @@ declare class DurableUserHub extends DurableObject<DurableUserHubEnv> {
|
|
|
1959
1994
|
/** Perform a privileged fetch with the grant's credential injected. */
|
|
1960
1995
|
private grantFetch;
|
|
1961
1996
|
private handleGrantFetch;
|
|
1962
|
-
webSocketMessage(ws: WebSocket,
|
|
1997
|
+
webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void>;
|
|
1963
1998
|
webSocketError(ws: WebSocket): Promise<void>;
|
|
1964
1999
|
/** Whether a persistent client (daemon) is connected right now. */
|
|
1965
2000
|
hasClient(): Promise<boolean>;
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { m as AgentBuilderEnv, n as AgentBuilderEvent, A as AgentDefinition, ah as BroadcastOptions, C as CodeExecutionBridge, h as DurableAgentBuilder, D as DurableThread, l as DurableUserHub, af as FlowStateSdk, ai as FlowStateWithSdk, G as HandlerSide, H as HubClientInfo, E as HumanSide, aq as ImageContextConfig, ae as InjectMessageOptions, as as LLMProviderInterface, M as ModelDefinition, aL as NamespaceResolutionError, w as PromptContent, s as PromptDefinition, y as PromptIncludePart, x as PromptPart, F as PromptSide, t as PromptToolConfig, P as Provider, at as ProviderContentPart, aw as ProviderFilePart, av as ProviderImagePart, ar as ProviderRegistry, au as ProviderTextPart, B as SessionToolBinding, z as SessionToolConfig, I as SideConfig, J as SubagentRelationship, u as SubagentToolConfig, S as SubpromptConfig, o as ThreadEventPayload, p as ThreadRegistryEntry, T as ThreadWatchdog, j as ThreadWatchdogState, v as ToolConfig, U as UpdateThreadParams, q as User, k as WatchdogCheckin, W as WatchdogProbeResult, f as applyRuntimeCors, am as buildImageDescription, c as canAccessThread, a9 as cat, aJ as createNamespaceContext, Q as emitThreadEvent, ag as enhanceFlowState, a0 as exists, ad as find, R as forceTurn, ap as generateImageDescription, a7 as getFileStats, N as getMessages, al as getMessagesToSummarize, az as getShortName, a8 as getThumbnail, ak as getUnsummarizedImageAttachments, aG as getVisibleAgentNames, aI as getVisibleModelNames, aH as getVisiblePromptNames, aF as getVisibleToolNames, ac as grep, aj as hasImageAttachments, aa as head, L as injectMessage, d as isAdminOnlyApiRoute, i as isHostedInstance, b as isPublicApiRoute, aK as isQualifiedName, e as isSameOriginBrowserRequest, a as isTrustedLocalDevelopmentRequest, Z as linkFile, a2 as mkdir, a5 as move, ao as optimizeImageContext, ax as parseQualifiedName, ay as qualifyName, K as queueTool, _ as readFile, a3 as readdir, O as reloadHistory, a6 as rename, an as replaceImagesWithDescriptions, aD as resolveAgent, aE as resolveHook, aC as resolveModel, aB as resolvePrompt, aA as resolveTool, a4 as rmdir, r as runtimeCorsHeaders, g as runtimePreflightResponse, $ as stat, ab as tail, a1 as unlink, V as updateThread, X as writeFile, Y as writeImage } from './resolver-
|
|
1
|
+
export { m as AgentBuilderEnv, n as AgentBuilderEvent, A as AgentDefinition, ah as BroadcastOptions, C as CodeExecutionBridge, h as DurableAgentBuilder, D as DurableThread, l as DurableUserHub, af as FlowStateSdk, ai as FlowStateWithSdk, G as HandlerSide, H as HubClientInfo, E as HumanSide, aq as ImageContextConfig, ae as InjectMessageOptions, as as LLMProviderInterface, M as ModelDefinition, aL as NamespaceResolutionError, w as PromptContent, s as PromptDefinition, y as PromptIncludePart, x as PromptPart, F as PromptSide, t as PromptToolConfig, P as Provider, at as ProviderContentPart, aw as ProviderFilePart, av as ProviderImagePart, ar as ProviderRegistry, au as ProviderTextPart, B as SessionToolBinding, z as SessionToolConfig, I as SideConfig, J as SubagentRelationship, u as SubagentToolConfig, S as SubpromptConfig, o as ThreadEventPayload, p as ThreadRegistryEntry, T as ThreadWatchdog, j as ThreadWatchdogState, v as ToolConfig, U as UpdateThreadParams, q as User, k as WatchdogCheckin, W as WatchdogProbeResult, f as applyRuntimeCors, am as buildImageDescription, c as canAccessThread, a9 as cat, aJ as createNamespaceContext, Q as emitThreadEvent, ag as enhanceFlowState, a0 as exists, ad as find, R as forceTurn, ap as generateImageDescription, a7 as getFileStats, N as getMessages, al as getMessagesToSummarize, az as getShortName, a8 as getThumbnail, ak as getUnsummarizedImageAttachments, aG as getVisibleAgentNames, aI as getVisibleModelNames, aH as getVisiblePromptNames, aF as getVisibleToolNames, ac as grep, aj as hasImageAttachments, aa as head, L as injectMessage, d as isAdminOnlyApiRoute, i as isHostedInstance, b as isPublicApiRoute, aK as isQualifiedName, e as isSameOriginBrowserRequest, a as isTrustedLocalDevelopmentRequest, Z as linkFile, a2 as mkdir, a5 as move, ao as optimizeImageContext, ax as parseQualifiedName, ay as qualifyName, K as queueTool, _ as readFile, a3 as readdir, O as reloadHistory, a6 as rename, an as replaceImagesWithDescriptions, aD as resolveAgent, aE as resolveHook, aC as resolveModel, aB as resolvePrompt, aA as resolveTool, a4 as rmdir, r as runtimeCorsHeaders, g as runtimePreflightResponse, $ as stat, ab as tail, a1 as unlink, V as updateThread, X as writeFile, Y as writeImage } from './resolver-DWuGxI4w.js';
|
|
2
2
|
export { v as Agent, J as AttachmentRef, i as AuthContext, A as AuthUser, e as BuilderThreadEndpointHandler, C as CLIENT_STREAM_CHUNK_BYTES, t as ClientDuplex, u as ClientForwarding, p as ClientStreamRequest, q as ClientStreamResponse, n as ClientToolRequest, o as ClientToolResponse, B as Controller, a as ControllerContext, G as Env, H as FileRecord, N as FileStats, D as FlowResult, F as FlowState, K as GrepResult, P as ImageContentPart, I as ImageMetadata, L as LLMResponse, M as Message, U as MessageContent, Q as MultimodalContent, R as RequestContext, S as StorageBackend, E as TelemetryEvent, O as TextContentPart, T as ThreadEndpointContext, b as ThreadEnv, w as ThreadInstance, x as ThreadMetadata, y as ToolCall, z as ToolResult, f as authenticate, j as callClient, k as callClientStream, c as createThreadEndpointHandler, m as decodeStreamChunk, d as defineController, l as encodeStreamChunk, h as endpointCallerFromAuth, g as requireAdmin, r as requireAuth, s as supportsClientForwarding } from './index-ChHAVogf.js';
|
|
3
3
|
export { AgentType, DefineHandlerOptions, DefinitionLoader, GlobalNamespaceContext, HANDLER_SYMBOL, HandlerDefinition, HookExecutionContext, HookExecutionSource, HookSignatures, ImageContent, ModelCapabilities, ModelProvider, NamespaceContext, PackageSignature, PackedExports, PackedMeta, PackedMetadata, PackedNamespaceContext, ParentHookRequirement, ParentHookStage, PromptInput, PromptTextPart, ProviderAssistantMessage, ProviderError, ProviderErrorCode, ProviderFactory, ProviderFactoryConfig, ProviderFinishReason, ProviderGeneratedImage, ProviderMessage, ProviderMessageContent, ModelCapabilities as ProviderModelCapabilities, ProviderReasoningDetail, ProviderRequest, ProviderResponse, ProviderStreamChunk, ProviderSystemMessage, ProviderTool, ProviderToolCallPart, ProviderToolMessage, ProviderToolResultContent, ProviderUsage, ProviderUserMessage, ReasoningConfig, StructuredPrompt, TextContent, ThreadHookName, Tool, ToolArgs, ToolArgsNode, ToolArgsRawShape, ToolContent, belongsToPackage, defineAgent, defineHandler, defineHook, defineModel, definePrompt, defineProvider, defineTool, isHandler, isPacked, isVisibleInNamespace, mapReasoningLevel, providerConst, providerEnv, providerValue } from '@standardagents/spec';
|
|
4
4
|
export { N as NamespacedRegistry } from './types-Dhd0M369.js';
|
package/dist/runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getProviderModelPricing } from '@standardagents/provider-pricing';
|
|
2
2
|
import { jsonPathReader } from '@formkit/jsonreader';
|
|
3
|
-
import { ProviderError, mapReasoningLevel, validateAgentDefinition, THREAD_STREAM_REASONING_PARAM, isUserEndpoint } from '@standardagents/spec';
|
|
3
|
+
import { ProviderError, mapReasoningLevel, validateAgentDefinition, THREAD_STREAM_REASONING_PARAM, isUserRoom, isUserEndpoint } from '@standardagents/spec';
|
|
4
4
|
export { HANDLER_SYMBOL, ProviderError, belongsToPackage, defineAgent, defineHandler, defineHook, defineModel, definePrompt, defineProvider, defineTool, isHandler, isPacked, isVisibleInNamespace, mapReasoningLevel, providerConst, providerEnv, providerValue } from '@standardagents/spec';
|
|
5
5
|
import 'virtual:@standardagents-providers';
|
|
6
6
|
import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
|
|
@@ -30547,6 +30547,12 @@ var UserStateImpl = class {
|
|
|
30547
30547
|
emit(event, data) {
|
|
30548
30548
|
this.host.broadcastUserEvent(this.userId, event, data);
|
|
30549
30549
|
}
|
|
30550
|
+
async clients() {
|
|
30551
|
+
return this.host.listRoomClients(this.userId);
|
|
30552
|
+
}
|
|
30553
|
+
async send(clientId, frame) {
|
|
30554
|
+
return this.host.sendToRoomClient(this.userId, clientId, frame);
|
|
30555
|
+
}
|
|
30550
30556
|
async listThreads(options) {
|
|
30551
30557
|
const { threads, total } = await this.singleton().listThreads({
|
|
30552
30558
|
user_id: this.userId,
|
|
@@ -30617,6 +30623,69 @@ function createExecutionChain() {
|
|
|
30617
30623
|
// src/durable-objects/DurableUserHub.ts
|
|
30618
30624
|
init_bridge_owner();
|
|
30619
30625
|
|
|
30626
|
+
// src/users/room.ts
|
|
30627
|
+
var ROOM_FRAME_MAX_BYTES = 1024 * 1024;
|
|
30628
|
+
var ROOM_FRAME_RATE_PER_SECOND = 200;
|
|
30629
|
+
var ROOM_FRAME_BURST = 400;
|
|
30630
|
+
var ROOM_SEND_BUDGET_BYTES = 8 * 1024 * 1024;
|
|
30631
|
+
var ROOM_SEND_BUDGET_WINDOW_MS = 1e4;
|
|
30632
|
+
var ROOM_PRESENCE_GRACE_MS = 3e4;
|
|
30633
|
+
var ROOM_CLOSE_SUPERSEDED = 4001;
|
|
30634
|
+
var ROOM_CLOSE_TOO_LARGE = 1009;
|
|
30635
|
+
var ROOM_CLOSE_TRY_AGAIN = 1013;
|
|
30636
|
+
function parseRoomFrame(message) {
|
|
30637
|
+
const size = typeof message === "string" ? message.length : message.byteLength;
|
|
30638
|
+
if (size > ROOM_FRAME_MAX_BYTES) return "too_large";
|
|
30639
|
+
if (typeof message !== "string") return null;
|
|
30640
|
+
let parsed;
|
|
30641
|
+
try {
|
|
30642
|
+
parsed = JSON.parse(message);
|
|
30643
|
+
} catch {
|
|
30644
|
+
return null;
|
|
30645
|
+
}
|
|
30646
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
30647
|
+
const frame = parsed;
|
|
30648
|
+
if (frame.type !== "room" || typeof frame.id !== "string" || frame.id.length === 0 || frame.id.length > 256) {
|
|
30649
|
+
return null;
|
|
30650
|
+
}
|
|
30651
|
+
return frame;
|
|
30652
|
+
}
|
|
30653
|
+
var RoomRateLimiter = class {
|
|
30654
|
+
constructor(ratePerSecond = ROOM_FRAME_RATE_PER_SECOND, burst = ROOM_FRAME_BURST, now = Date.now()) {
|
|
30655
|
+
this.ratePerSecond = ratePerSecond;
|
|
30656
|
+
this.burst = burst;
|
|
30657
|
+
this.tokens = burst;
|
|
30658
|
+
this.last = now;
|
|
30659
|
+
}
|
|
30660
|
+
tokens;
|
|
30661
|
+
last;
|
|
30662
|
+
take(now = Date.now()) {
|
|
30663
|
+
const elapsed = Math.max(0, now - this.last) / 1e3;
|
|
30664
|
+
this.tokens = Math.min(this.burst, this.tokens + elapsed * this.ratePerSecond);
|
|
30665
|
+
this.last = now;
|
|
30666
|
+
if (this.tokens < 1) return false;
|
|
30667
|
+
this.tokens -= 1;
|
|
30668
|
+
return true;
|
|
30669
|
+
}
|
|
30670
|
+
};
|
|
30671
|
+
var RoomSendBudget = class {
|
|
30672
|
+
constructor(budgetBytes = ROOM_SEND_BUDGET_BYTES, windowMs = ROOM_SEND_BUDGET_WINDOW_MS, now = Date.now()) {
|
|
30673
|
+
this.budgetBytes = budgetBytes;
|
|
30674
|
+
this.windowMs = windowMs;
|
|
30675
|
+
this.windowStart = now;
|
|
30676
|
+
}
|
|
30677
|
+
spent = 0;
|
|
30678
|
+
windowStart;
|
|
30679
|
+
charge(bytes, now = Date.now()) {
|
|
30680
|
+
if (now - this.windowStart >= this.windowMs) {
|
|
30681
|
+
this.windowStart = now;
|
|
30682
|
+
this.spent = 0;
|
|
30683
|
+
}
|
|
30684
|
+
this.spent += bytes;
|
|
30685
|
+
return this.spent <= this.budgetBytes;
|
|
30686
|
+
}
|
|
30687
|
+
};
|
|
30688
|
+
|
|
30620
30689
|
// src/users/grants.ts
|
|
30621
30690
|
var INSTANCE_GRANTS_MAX_BYTES = 64 * 1024;
|
|
30622
30691
|
var INSTANCE_GRANTS_MAX_COUNT = 64;
|
|
@@ -30772,6 +30841,12 @@ var DurableUserHub = class extends DurableObject {
|
|
|
30772
30841
|
clientSockets = /* @__PURE__ */ new Set();
|
|
30773
30842
|
clientInfo = /* @__PURE__ */ new Map();
|
|
30774
30843
|
migrated = false;
|
|
30844
|
+
/** Inbound frame rate and outbound byte budget per room socket. */
|
|
30845
|
+
roomLimits = /* @__PURE__ */ new Map();
|
|
30846
|
+
/** Detach notices held for the presence grace window, by client id. */
|
|
30847
|
+
pendingDetach = /* @__PURE__ */ new Map();
|
|
30848
|
+
/** The project's room definition, loaded once per instance. */
|
|
30849
|
+
roomDefinition = null;
|
|
30775
30850
|
constructor(ctx, env) {
|
|
30776
30851
|
super(ctx, env);
|
|
30777
30852
|
ctx.blockConcurrencyWhile(async () => {
|
|
@@ -30784,7 +30859,8 @@ var DurableUserHub = class extends DurableObject {
|
|
|
30784
30859
|
this.clientInfo.set(ws, {
|
|
30785
30860
|
clientId: typeof attachment.clientId === "string" ? attachment.clientId : null,
|
|
30786
30861
|
clientName: typeof attachment.clientName === "string" ? attachment.clientName : null,
|
|
30787
|
-
clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null
|
|
30862
|
+
clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null,
|
|
30863
|
+
since: typeof attachment.since === "number" ? attachment.since : void 0
|
|
30788
30864
|
});
|
|
30789
30865
|
}
|
|
30790
30866
|
}
|
|
@@ -30857,20 +30933,188 @@ var DurableUserHub = class extends DurableObject {
|
|
|
30857
30933
|
if (request.headers.get("Upgrade") === "websocket") {
|
|
30858
30934
|
const pair = new WebSocketPair();
|
|
30859
30935
|
const [client, server] = [pair[0], pair[1]];
|
|
30860
|
-
const info = parseBridgeClientParams(url).info;
|
|
30936
|
+
const info = { ...parseBridgeClientParams(url).info, since: Date.now() };
|
|
30861
30937
|
this.ensureHeartbeatAutoResponse();
|
|
30862
30938
|
this.ctx.acceptWebSocket(server);
|
|
30863
30939
|
server.serializeAttachment({ type: "hub_client", ...info });
|
|
30940
|
+
const reconnecting = info.clientId !== null && this.supersedeClient(info.clientId, server);
|
|
30864
30941
|
this.clientSockets.add(server);
|
|
30865
30942
|
this.clientInfo.set(server, info);
|
|
30866
30943
|
try {
|
|
30867
30944
|
server.send(JSON.stringify({ type: "hub_ready" }));
|
|
30868
30945
|
} catch {
|
|
30869
30946
|
}
|
|
30947
|
+
if (info.clientId) {
|
|
30948
|
+
const connection = this.connectionOf(info);
|
|
30949
|
+
if (connection) this.observeAttach(connection, reconnecting);
|
|
30950
|
+
}
|
|
30870
30951
|
return new Response(null, { status: 101, webSocket: client });
|
|
30871
30952
|
}
|
|
30872
30953
|
return new Response("Not found", { status: 404 });
|
|
30873
30954
|
}
|
|
30955
|
+
// ─── Room connections (spec: Users rooms) ───────────────────────────────────
|
|
30956
|
+
connectionOf(info) {
|
|
30957
|
+
if (!info?.clientId) return null;
|
|
30958
|
+
return { clientId: info.clientId, clientKind: info.clientKind, since: info.since ?? 0 };
|
|
30959
|
+
}
|
|
30960
|
+
/** Close every other socket presenting `clientId`; true when one existed. */
|
|
30961
|
+
supersedeClient(clientId, keep) {
|
|
30962
|
+
let superseded = false;
|
|
30963
|
+
for (const ws of Array.from(this.clientSockets)) {
|
|
30964
|
+
if (ws === keep || this.clientInfo.get(ws)?.clientId !== clientId) continue;
|
|
30965
|
+
this.forgetSocket(ws);
|
|
30966
|
+
superseded = true;
|
|
30967
|
+
try {
|
|
30968
|
+
ws.send(JSON.stringify({ type: "superseded" }));
|
|
30969
|
+
} catch {
|
|
30970
|
+
}
|
|
30971
|
+
try {
|
|
30972
|
+
ws.close(ROOM_CLOSE_SUPERSEDED, "Superseded by a newer connection from this client");
|
|
30973
|
+
} catch {
|
|
30974
|
+
}
|
|
30975
|
+
}
|
|
30976
|
+
return superseded;
|
|
30977
|
+
}
|
|
30978
|
+
forgetSocket(ws) {
|
|
30979
|
+
this.clientSockets.delete(ws);
|
|
30980
|
+
this.clientInfo.delete(ws);
|
|
30981
|
+
this.roomLimits.delete(ws);
|
|
30982
|
+
}
|
|
30983
|
+
liveSocketFor(clientId) {
|
|
30984
|
+
for (const ws of this.clientSockets) {
|
|
30985
|
+
if (this.clientInfo.get(ws)?.clientId === clientId && ws.readyState === WebSocket.OPEN) return ws;
|
|
30986
|
+
}
|
|
30987
|
+
return null;
|
|
30988
|
+
}
|
|
30989
|
+
/** Live room connections, one per client id, oldest attachment first. */
|
|
30990
|
+
listRoomClients() {
|
|
30991
|
+
const byId = /* @__PURE__ */ new Map();
|
|
30992
|
+
for (const ws of this.clientSockets) {
|
|
30993
|
+
if (ws.readyState !== WebSocket.OPEN) continue;
|
|
30994
|
+
const connection = this.connectionOf(this.clientInfo.get(ws));
|
|
30995
|
+
if (connection) byId.set(connection.clientId, connection);
|
|
30996
|
+
}
|
|
30997
|
+
return Array.from(byId.values()).sort((a, b) => a.since - b.since);
|
|
30998
|
+
}
|
|
30999
|
+
/** Deliver one frame to the socket carrying `clientId`, within its send budget. */
|
|
31000
|
+
sendToRoomClient(clientId, frame) {
|
|
31001
|
+
const ws = this.liveSocketFor(clientId);
|
|
31002
|
+
if (!ws) return false;
|
|
31003
|
+
return this.sendRoomFrame(ws, JSON.stringify(frame));
|
|
31004
|
+
}
|
|
31005
|
+
sendRoomFrame(ws, payload) {
|
|
31006
|
+
const limits = this.limitsFor(ws);
|
|
31007
|
+
if (!limits.send.charge(payload.length)) {
|
|
31008
|
+
this.forgetSocket(ws);
|
|
31009
|
+
try {
|
|
31010
|
+
ws.close(ROOM_CLOSE_TRY_AGAIN, "Slow consumer: send budget exceeded");
|
|
31011
|
+
} catch {
|
|
31012
|
+
}
|
|
31013
|
+
return false;
|
|
31014
|
+
}
|
|
31015
|
+
try {
|
|
31016
|
+
ws.send(payload);
|
|
31017
|
+
return true;
|
|
31018
|
+
} catch {
|
|
31019
|
+
this.forgetSocket(ws);
|
|
31020
|
+
return false;
|
|
31021
|
+
}
|
|
31022
|
+
}
|
|
31023
|
+
limitsFor(ws) {
|
|
31024
|
+
let limits = this.roomLimits.get(ws);
|
|
31025
|
+
if (!limits) {
|
|
31026
|
+
limits = { rate: new RoomRateLimiter(), send: new RoomSendBudget() };
|
|
31027
|
+
this.roomLimits.set(ws, limits);
|
|
31028
|
+
}
|
|
31029
|
+
return limits;
|
|
31030
|
+
}
|
|
31031
|
+
/** Load the project's room definition from the users registry, once. */
|
|
31032
|
+
async loadRoom() {
|
|
31033
|
+
if (!this.roomDefinition) {
|
|
31034
|
+
this.roomDefinition = (async () => {
|
|
31035
|
+
const registry = this.userEndpoints();
|
|
31036
|
+
for (const key of Object.keys(registry)) {
|
|
31037
|
+
if (!/(^|\/)room$/.test(key)) continue;
|
|
31038
|
+
try {
|
|
31039
|
+
const loaded = await registry[key]();
|
|
31040
|
+
const candidate = loaded.default ?? loaded;
|
|
31041
|
+
if (isUserRoom(candidate)) return candidate;
|
|
31042
|
+
} catch {
|
|
31043
|
+
}
|
|
31044
|
+
}
|
|
31045
|
+
return null;
|
|
31046
|
+
})();
|
|
31047
|
+
}
|
|
31048
|
+
return this.roomDefinition;
|
|
31049
|
+
}
|
|
31050
|
+
/** Whether `key` is room-owned state, exempt from `user_value_changed`. */
|
|
31051
|
+
async roomOwnsKey(key) {
|
|
31052
|
+
const room = await this.loadRoom();
|
|
31053
|
+
return Boolean(room?.valuePrefix && key.startsWith(room.valuePrefix));
|
|
31054
|
+
}
|
|
31055
|
+
/**
|
|
31056
|
+
* Presence runs inside the serialization chain, after any execution the
|
|
31057
|
+
* event raced with, so a room's presence write never interleaves with a
|
|
31058
|
+
* mutation. A reconnect inside the grace window is not a change.
|
|
31059
|
+
*/
|
|
31060
|
+
observeAttach(connection, reconnecting) {
|
|
31061
|
+
const pending = this.pendingDetach.get(connection.clientId);
|
|
31062
|
+
if (pending) {
|
|
31063
|
+
clearTimeout(pending);
|
|
31064
|
+
this.pendingDetach.delete(connection.clientId);
|
|
31065
|
+
return;
|
|
31066
|
+
}
|
|
31067
|
+
if (reconnecting) return;
|
|
31068
|
+
this.dispatchRoomPresence({ kind: "attached", connection, at: Date.now() });
|
|
31069
|
+
}
|
|
31070
|
+
observeDetach(connection) {
|
|
31071
|
+
if (this.liveSocketFor(connection.clientId)) return;
|
|
31072
|
+
const existing = this.pendingDetach.get(connection.clientId);
|
|
31073
|
+
if (existing) clearTimeout(existing);
|
|
31074
|
+
this.pendingDetach.set(
|
|
31075
|
+
connection.clientId,
|
|
31076
|
+
setTimeout(() => {
|
|
31077
|
+
this.pendingDetach.delete(connection.clientId);
|
|
31078
|
+
if (this.liveSocketFor(connection.clientId)) return;
|
|
31079
|
+
this.dispatchRoomPresence({ kind: "detached", connection, at: Date.now() });
|
|
31080
|
+
}, ROOM_PRESENCE_GRACE_MS)
|
|
31081
|
+
);
|
|
31082
|
+
}
|
|
31083
|
+
dispatchRoomPresence(event) {
|
|
31084
|
+
const run = this.executionChain.run(async () => {
|
|
31085
|
+
const room = await this.loadRoom();
|
|
31086
|
+
if (!room?.onPresence) return;
|
|
31087
|
+
const user = new UserStateImpl(this.roomUserId(), this.scopeHost(), { kind: "userRoom", name: "presence" });
|
|
31088
|
+
try {
|
|
31089
|
+
await room.onPresence(user, event);
|
|
31090
|
+
} catch (error) {
|
|
31091
|
+
console.error("[DurableUserHub] room presence handler failed:", error);
|
|
31092
|
+
}
|
|
31093
|
+
});
|
|
31094
|
+
this.ctx.waitUntil(run);
|
|
31095
|
+
}
|
|
31096
|
+
/** The hub is one object per user; its name is the user id. */
|
|
31097
|
+
roomUserId() {
|
|
31098
|
+
return this.ctx.id.name ?? this.ctx.id.toString();
|
|
31099
|
+
}
|
|
31100
|
+
async dispatchRoomFrame(ws, frame) {
|
|
31101
|
+
const connection = this.connectionOf(this.clientInfo.get(ws));
|
|
31102
|
+
if (!connection) return;
|
|
31103
|
+
await this.executionChain.run(async () => {
|
|
31104
|
+
const room = await this.loadRoom();
|
|
31105
|
+
if (!room) {
|
|
31106
|
+
this.sendRoomFrame(ws, JSON.stringify({ type: "room", id: frame.id, error: "no_room" }));
|
|
31107
|
+
return;
|
|
31108
|
+
}
|
|
31109
|
+
const user = new UserStateImpl(this.roomUserId(), this.scopeHost(), { kind: "userRoom", name: "frame" });
|
|
31110
|
+
try {
|
|
31111
|
+
await room.onFrame(user, connection, frame);
|
|
31112
|
+
} catch (error) {
|
|
31113
|
+
console.error("[DurableUserHub] room frame handler failed:", error);
|
|
31114
|
+
this.sendRoomFrame(ws, JSON.stringify({ type: "room", id: frame.id, error: "handler_failed" }));
|
|
31115
|
+
}
|
|
31116
|
+
});
|
|
31117
|
+
}
|
|
30874
31118
|
// ─── User scope: event stream (spec: Users §4) ─────────────────────────────
|
|
30875
31119
|
/** Idle sockets answer heartbeats at the edge without waking the object.
|
|
30876
31120
|
* Registered at accept time rather than in the constructor so a hub that
|
|
@@ -30908,8 +31152,9 @@ var DurableUserHub = class extends DurableObject {
|
|
|
30908
31152
|
return new Response(null, { status: 101, webSocket: client });
|
|
30909
31153
|
}
|
|
30910
31154
|
/**
|
|
30911
|
-
* Deliver an event frame to every socket attached to this user's stream
|
|
30912
|
-
* At-most-once, live-only (spec: Users §4) —
|
|
31155
|
+
* Deliver an event frame to every socket attached to this user's stream
|
|
31156
|
+
* and to every room socket. At-most-once, live-only (spec: Users §4) —
|
|
31157
|
+
* no history, no acks.
|
|
30913
31158
|
*/
|
|
30914
31159
|
broadcastUserEvent(userId, event, data) {
|
|
30915
31160
|
const frame = JSON.stringify({ event, data });
|
|
@@ -30921,6 +31166,10 @@ var DurableUserHub = class extends DurableObject {
|
|
|
30921
31166
|
} catch {
|
|
30922
31167
|
}
|
|
30923
31168
|
}
|
|
31169
|
+
for (const ws of Array.from(this.clientSockets)) {
|
|
31170
|
+
if (ws.readyState !== WebSocket.OPEN || !this.clientInfo.get(ws)?.clientId) continue;
|
|
31171
|
+
this.sendRoomFrame(ws, frame);
|
|
31172
|
+
}
|
|
30924
31173
|
}
|
|
30925
31174
|
// ─── User scope: serialized endpoint execution (spec: Users §2.1, §3) ─────
|
|
30926
31175
|
/** Per-user execution chain (spec: Users §2.1) — see users/serialize.ts. */
|
|
@@ -30931,6 +31180,8 @@ var DurableUserHub = class extends DurableObject {
|
|
|
30931
31180
|
setUserValue: (userId, key, value) => this.setUserValue(userId, key, value),
|
|
30932
31181
|
listUserValues: (userId, options) => this.listUserValues(userId, options ?? {}),
|
|
30933
31182
|
broadcastUserEvent: (userId, event, data) => this.broadcastUserEvent(userId, event, data),
|
|
31183
|
+
listRoomClients: () => this.listRoomClients(),
|
|
31184
|
+
sendToRoomClient: (_userId, clientId, frame) => this.sendToRoomClient(clientId, frame),
|
|
30934
31185
|
grantFetch: (userId, grant, req) => this.grantFetch(userId, grant, req),
|
|
30935
31186
|
env: this.env
|
|
30936
31187
|
};
|
|
@@ -30950,6 +31201,7 @@ var DurableUserHub = class extends DurableObject {
|
|
|
30950
31201
|
}
|
|
30951
31202
|
const module = loaded;
|
|
30952
31203
|
const handler = module.default ?? loaded;
|
|
31204
|
+
if (isUserRoom(handler)) return Response.json({ error: "Not found" }, { status: 404 });
|
|
30953
31205
|
if (!isUserEndpoint(handler)) {
|
|
30954
31206
|
return Response.json(
|
|
30955
31207
|
{ error: `${match.key} is not a defineUserEndpoint export` },
|
|
@@ -30993,6 +31245,7 @@ var DurableUserHub = class extends DurableObject {
|
|
|
30993
31245
|
const names = Object.keys(registry);
|
|
30994
31246
|
if (names.length === 0) return;
|
|
30995
31247
|
const run = async () => {
|
|
31248
|
+
if (hookType === "user_value_changed" && await this.roomOwnsKey(String(args[0]))) return;
|
|
30996
31249
|
const user = new UserStateImpl(userId, this.scopeHost(), {
|
|
30997
31250
|
kind: "userHook",
|
|
30998
31251
|
name: hookType
|
|
@@ -31017,8 +31270,9 @@ var DurableUserHub = class extends DurableObject {
|
|
|
31017
31270
|
this.ctx.waitUntil(run());
|
|
31018
31271
|
}
|
|
31019
31272
|
async webSocketClose(ws) {
|
|
31020
|
-
this.
|
|
31021
|
-
this.
|
|
31273
|
+
const connection = this.connectionOf(this.clientInfo.get(ws));
|
|
31274
|
+
this.forgetSocket(ws);
|
|
31275
|
+
if (connection) this.observeDetach(connection);
|
|
31022
31276
|
const attachment = ws.deserializeAttachment();
|
|
31023
31277
|
if (attachment?.type === "user_stream") {
|
|
31024
31278
|
this.dispatchUserHooks(String(attachment.userId), "user_client_detached", [
|
|
@@ -31147,7 +31401,7 @@ var DurableUserHub = class extends DurableObject {
|
|
|
31147
31401
|
caller: body.caller ?? { kind: "unknown", name: "unknown" }
|
|
31148
31402
|
});
|
|
31149
31403
|
}
|
|
31150
|
-
async webSocketMessage(ws,
|
|
31404
|
+
async webSocketMessage(ws, message) {
|
|
31151
31405
|
const attachment = ws.deserializeAttachment();
|
|
31152
31406
|
if (attachment?.type !== "hub_client") return;
|
|
31153
31407
|
if (!this.clientSockets.has(ws)) {
|
|
@@ -31155,9 +31409,29 @@ var DurableUserHub = class extends DurableObject {
|
|
|
31155
31409
|
this.clientInfo.set(ws, {
|
|
31156
31410
|
clientId: typeof attachment.clientId === "string" ? attachment.clientId : null,
|
|
31157
31411
|
clientName: typeof attachment.clientName === "string" ? attachment.clientName : null,
|
|
31158
|
-
clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null
|
|
31412
|
+
clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null,
|
|
31413
|
+
since: typeof attachment.since === "number" ? attachment.since : void 0
|
|
31159
31414
|
});
|
|
31160
31415
|
}
|
|
31416
|
+
if (!this.limitsFor(ws).rate.take()) {
|
|
31417
|
+
this.forgetSocket(ws);
|
|
31418
|
+
try {
|
|
31419
|
+
ws.close(ROOM_CLOSE_TRY_AGAIN, "Frame rate exceeded");
|
|
31420
|
+
} catch {
|
|
31421
|
+
}
|
|
31422
|
+
return;
|
|
31423
|
+
}
|
|
31424
|
+
const frame = parseRoomFrame(message);
|
|
31425
|
+
if (frame === "too_large") {
|
|
31426
|
+
this.forgetSocket(ws);
|
|
31427
|
+
try {
|
|
31428
|
+
ws.close(ROOM_CLOSE_TOO_LARGE, "Frame exceeds 1 MiB");
|
|
31429
|
+
} catch {
|
|
31430
|
+
}
|
|
31431
|
+
return;
|
|
31432
|
+
}
|
|
31433
|
+
if (frame === null || !this.clientInfo.get(ws)?.clientId) return;
|
|
31434
|
+
await this.dispatchRoomFrame(ws, frame);
|
|
31161
31435
|
}
|
|
31162
31436
|
async webSocketError(ws) {
|
|
31163
31437
|
await this.webSocketClose(ws);
|