@standardagents/builder 0.41.8 → 0.41.10
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 +291 -9
- package/dist/index.js.map +1 -1
- package/dist/{resolver-BMZAzKD3.d.ts → resolver-kvVdxct2.d.ts} +41 -4
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +291 -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,22 @@ 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, and its
|
|
1908
|
+
* registry key, which names room executions for grant policies. */
|
|
1909
|
+
private roomDefinition;
|
|
1910
|
+
private roomKey;
|
|
1901
1911
|
constructor(ctx: DurableObjectState, env: DurableUserHubEnv);
|
|
1902
1912
|
private ensureMigrated;
|
|
1903
1913
|
/**
|
|
@@ -1920,6 +1930,32 @@ declare class DurableUserHub extends DurableObject<DurableUserHubEnv> {
|
|
|
1920
1930
|
* opened separately and only while a thread is active.
|
|
1921
1931
|
*/
|
|
1922
1932
|
fetch(request: Request): Promise<Response>;
|
|
1933
|
+
private connectionOf;
|
|
1934
|
+
/** Close every other socket presenting `clientId`; true when one existed. */
|
|
1935
|
+
private supersedeClient;
|
|
1936
|
+
private forgetSocket;
|
|
1937
|
+
private liveSocketFor;
|
|
1938
|
+
/** Live room connections, one per client id, oldest attachment first. */
|
|
1939
|
+
listRoomClients(): UserRoomConnection[];
|
|
1940
|
+
/** Deliver one frame to the socket carrying `clientId`, within its send budget. */
|
|
1941
|
+
sendToRoomClient(clientId: string, frame: unknown): boolean;
|
|
1942
|
+
private sendRoomFrame;
|
|
1943
|
+
private limitsFor;
|
|
1944
|
+
/** Load the project's room definition from the users registry, once. */
|
|
1945
|
+
private loadRoom;
|
|
1946
|
+
/** Whether `key` is room-owned state, exempt from `user_value_changed`. */
|
|
1947
|
+
private roomOwnsKey;
|
|
1948
|
+
/**
|
|
1949
|
+
* Presence runs inside the serialization chain, after any execution the
|
|
1950
|
+
* event raced with, so a room's presence write never interleaves with a
|
|
1951
|
+
* mutation. A reconnect inside the grace window is not a change.
|
|
1952
|
+
*/
|
|
1953
|
+
private observeAttach;
|
|
1954
|
+
private observeDetach;
|
|
1955
|
+
private dispatchRoomPresence;
|
|
1956
|
+
/** The hub is one object per user; its name is the user id. */
|
|
1957
|
+
private roomUserId;
|
|
1958
|
+
private dispatchRoomFrame;
|
|
1923
1959
|
/** Idle sockets answer heartbeats at the edge without waking the object.
|
|
1924
1960
|
* Registered at accept time rather than in the constructor so a hub that
|
|
1925
1961
|
* never accepts a socket stays fully evictable (this also keeps
|
|
@@ -1929,8 +1965,9 @@ declare class DurableUserHub extends DurableObject<DurableUserHubEnv> {
|
|
|
1929
1965
|
private streamSockets;
|
|
1930
1966
|
private acceptUserStream;
|
|
1931
1967
|
/**
|
|
1932
|
-
* Deliver an event frame to every socket attached to this user's stream
|
|
1933
|
-
* At-most-once, live-only (spec: Users §4) —
|
|
1968
|
+
* Deliver an event frame to every socket attached to this user's stream
|
|
1969
|
+
* and to every room socket. At-most-once, live-only (spec: Users §4) —
|
|
1970
|
+
* no history, no acks.
|
|
1934
1971
|
*/
|
|
1935
1972
|
broadcastUserEvent(userId: string, event: string, data?: unknown): void;
|
|
1936
1973
|
/** Per-user execution chain (spec: Users §2.1) — see users/serialize.ts. */
|
|
@@ -1959,7 +1996,7 @@ declare class DurableUserHub extends DurableObject<DurableUserHubEnv> {
|
|
|
1959
1996
|
/** Perform a privileged fetch with the grant's credential injected. */
|
|
1960
1997
|
private grantFetch;
|
|
1961
1998
|
private handleGrantFetch;
|
|
1962
|
-
webSocketMessage(ws: WebSocket,
|
|
1999
|
+
webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void>;
|
|
1963
2000
|
webSocketError(ws: WebSocket): Promise<void>;
|
|
1964
2001
|
/** Whether a persistent client (daemon) is connected right now. */
|
|
1965
2002
|
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-kvVdxct2.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;
|
|
@@ -30739,6 +30808,9 @@ function callerAllowed(allow, caller) {
|
|
|
30739
30808
|
endpoint: allow.endpoints,
|
|
30740
30809
|
userEndpoint: allow.userEndpoints,
|
|
30741
30810
|
userHook: allow.userEndpoints,
|
|
30811
|
+
// A room execution is policy-named by its registry key; a policy may
|
|
30812
|
+
// list it under `userRooms` or alongside the endpoints it serves.
|
|
30813
|
+
userRoom: allow.userRooms ?? allow.userEndpoints,
|
|
30742
30814
|
agent: allow.agents
|
|
30743
30815
|
};
|
|
30744
30816
|
const list = lists[caller.kind];
|
|
@@ -30772,6 +30844,14 @@ var DurableUserHub = class extends DurableObject {
|
|
|
30772
30844
|
clientSockets = /* @__PURE__ */ new Set();
|
|
30773
30845
|
clientInfo = /* @__PURE__ */ new Map();
|
|
30774
30846
|
migrated = false;
|
|
30847
|
+
/** Inbound frame rate and outbound byte budget per room socket. */
|
|
30848
|
+
roomLimits = /* @__PURE__ */ new Map();
|
|
30849
|
+
/** Detach notices held for the presence grace window, by client id. */
|
|
30850
|
+
pendingDetach = /* @__PURE__ */ new Map();
|
|
30851
|
+
/** The project's room definition, loaded once per instance, and its
|
|
30852
|
+
* registry key, which names room executions for grant policies. */
|
|
30853
|
+
roomDefinition = null;
|
|
30854
|
+
roomKey = null;
|
|
30775
30855
|
constructor(ctx, env) {
|
|
30776
30856
|
super(ctx, env);
|
|
30777
30857
|
ctx.blockConcurrencyWhile(async () => {
|
|
@@ -30784,7 +30864,8 @@ var DurableUserHub = class extends DurableObject {
|
|
|
30784
30864
|
this.clientInfo.set(ws, {
|
|
30785
30865
|
clientId: typeof attachment.clientId === "string" ? attachment.clientId : null,
|
|
30786
30866
|
clientName: typeof attachment.clientName === "string" ? attachment.clientName : null,
|
|
30787
|
-
clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null
|
|
30867
|
+
clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null,
|
|
30868
|
+
since: typeof attachment.since === "number" ? attachment.since : void 0
|
|
30788
30869
|
});
|
|
30789
30870
|
}
|
|
30790
30871
|
}
|
|
@@ -30857,20 +30938,191 @@ var DurableUserHub = class extends DurableObject {
|
|
|
30857
30938
|
if (request.headers.get("Upgrade") === "websocket") {
|
|
30858
30939
|
const pair = new WebSocketPair();
|
|
30859
30940
|
const [client, server] = [pair[0], pair[1]];
|
|
30860
|
-
const info = parseBridgeClientParams(url).info;
|
|
30941
|
+
const info = { ...parseBridgeClientParams(url).info, since: Date.now() };
|
|
30861
30942
|
this.ensureHeartbeatAutoResponse();
|
|
30862
30943
|
this.ctx.acceptWebSocket(server);
|
|
30863
30944
|
server.serializeAttachment({ type: "hub_client", ...info });
|
|
30945
|
+
const reconnecting = info.clientId !== null && this.supersedeClient(info.clientId, server);
|
|
30864
30946
|
this.clientSockets.add(server);
|
|
30865
30947
|
this.clientInfo.set(server, info);
|
|
30866
30948
|
try {
|
|
30867
30949
|
server.send(JSON.stringify({ type: "hub_ready" }));
|
|
30868
30950
|
} catch {
|
|
30869
30951
|
}
|
|
30952
|
+
if (info.clientId) {
|
|
30953
|
+
const connection = this.connectionOf(info);
|
|
30954
|
+
if (connection) this.observeAttach(connection, reconnecting);
|
|
30955
|
+
}
|
|
30870
30956
|
return new Response(null, { status: 101, webSocket: client });
|
|
30871
30957
|
}
|
|
30872
30958
|
return new Response("Not found", { status: 404 });
|
|
30873
30959
|
}
|
|
30960
|
+
// ─── Room connections (spec: Users rooms) ───────────────────────────────────
|
|
30961
|
+
connectionOf(info) {
|
|
30962
|
+
if (!info?.clientId) return null;
|
|
30963
|
+
return { clientId: info.clientId, clientKind: info.clientKind, since: info.since ?? 0 };
|
|
30964
|
+
}
|
|
30965
|
+
/** Close every other socket presenting `clientId`; true when one existed. */
|
|
30966
|
+
supersedeClient(clientId, keep) {
|
|
30967
|
+
let superseded = false;
|
|
30968
|
+
for (const ws of Array.from(this.clientSockets)) {
|
|
30969
|
+
if (ws === keep || this.clientInfo.get(ws)?.clientId !== clientId) continue;
|
|
30970
|
+
this.forgetSocket(ws);
|
|
30971
|
+
superseded = true;
|
|
30972
|
+
try {
|
|
30973
|
+
ws.send(JSON.stringify({ type: "superseded" }));
|
|
30974
|
+
} catch {
|
|
30975
|
+
}
|
|
30976
|
+
try {
|
|
30977
|
+
ws.close(ROOM_CLOSE_SUPERSEDED, "Superseded by a newer connection from this client");
|
|
30978
|
+
} catch {
|
|
30979
|
+
}
|
|
30980
|
+
}
|
|
30981
|
+
return superseded;
|
|
30982
|
+
}
|
|
30983
|
+
forgetSocket(ws) {
|
|
30984
|
+
this.clientSockets.delete(ws);
|
|
30985
|
+
this.clientInfo.delete(ws);
|
|
30986
|
+
this.roomLimits.delete(ws);
|
|
30987
|
+
}
|
|
30988
|
+
liveSocketFor(clientId) {
|
|
30989
|
+
for (const ws of this.clientSockets) {
|
|
30990
|
+
if (this.clientInfo.get(ws)?.clientId === clientId && ws.readyState === WebSocket.OPEN) return ws;
|
|
30991
|
+
}
|
|
30992
|
+
return null;
|
|
30993
|
+
}
|
|
30994
|
+
/** Live room connections, one per client id, oldest attachment first. */
|
|
30995
|
+
listRoomClients() {
|
|
30996
|
+
const byId = /* @__PURE__ */ new Map();
|
|
30997
|
+
for (const ws of this.clientSockets) {
|
|
30998
|
+
if (ws.readyState !== WebSocket.OPEN) continue;
|
|
30999
|
+
const connection = this.connectionOf(this.clientInfo.get(ws));
|
|
31000
|
+
if (connection) byId.set(connection.clientId, connection);
|
|
31001
|
+
}
|
|
31002
|
+
return Array.from(byId.values()).sort((a, b) => a.since - b.since);
|
|
31003
|
+
}
|
|
31004
|
+
/** Deliver one frame to the socket carrying `clientId`, within its send budget. */
|
|
31005
|
+
sendToRoomClient(clientId, frame) {
|
|
31006
|
+
const ws = this.liveSocketFor(clientId);
|
|
31007
|
+
if (!ws) return false;
|
|
31008
|
+
return this.sendRoomFrame(ws, JSON.stringify(frame));
|
|
31009
|
+
}
|
|
31010
|
+
sendRoomFrame(ws, payload) {
|
|
31011
|
+
const limits = this.limitsFor(ws);
|
|
31012
|
+
if (!limits.send.charge(payload.length)) {
|
|
31013
|
+
this.forgetSocket(ws);
|
|
31014
|
+
try {
|
|
31015
|
+
ws.close(ROOM_CLOSE_TRY_AGAIN, "Slow consumer: send budget exceeded");
|
|
31016
|
+
} catch {
|
|
31017
|
+
}
|
|
31018
|
+
return false;
|
|
31019
|
+
}
|
|
31020
|
+
try {
|
|
31021
|
+
ws.send(payload);
|
|
31022
|
+
return true;
|
|
31023
|
+
} catch {
|
|
31024
|
+
this.forgetSocket(ws);
|
|
31025
|
+
return false;
|
|
31026
|
+
}
|
|
31027
|
+
}
|
|
31028
|
+
limitsFor(ws) {
|
|
31029
|
+
let limits = this.roomLimits.get(ws);
|
|
31030
|
+
if (!limits) {
|
|
31031
|
+
limits = { rate: new RoomRateLimiter(), send: new RoomSendBudget() };
|
|
31032
|
+
this.roomLimits.set(ws, limits);
|
|
31033
|
+
}
|
|
31034
|
+
return limits;
|
|
31035
|
+
}
|
|
31036
|
+
/** Load the project's room definition from the users registry, once. */
|
|
31037
|
+
async loadRoom() {
|
|
31038
|
+
if (!this.roomDefinition) {
|
|
31039
|
+
this.roomDefinition = (async () => {
|
|
31040
|
+
const registry = this.userEndpoints();
|
|
31041
|
+
for (const key of Object.keys(registry)) {
|
|
31042
|
+
if (!/(^|\/)room$/.test(key)) continue;
|
|
31043
|
+
try {
|
|
31044
|
+
const loaded = await registry[key]();
|
|
31045
|
+
const candidate = loaded.default ?? loaded;
|
|
31046
|
+
if (isUserRoom(candidate)) {
|
|
31047
|
+
this.roomKey = key;
|
|
31048
|
+
return candidate;
|
|
31049
|
+
}
|
|
31050
|
+
} catch {
|
|
31051
|
+
}
|
|
31052
|
+
}
|
|
31053
|
+
return null;
|
|
31054
|
+
})();
|
|
31055
|
+
}
|
|
31056
|
+
return this.roomDefinition;
|
|
31057
|
+
}
|
|
31058
|
+
/** Whether `key` is room-owned state, exempt from `user_value_changed`. */
|
|
31059
|
+
async roomOwnsKey(key) {
|
|
31060
|
+
const room = await this.loadRoom();
|
|
31061
|
+
return Boolean(room?.valuePrefix && key.startsWith(room.valuePrefix));
|
|
31062
|
+
}
|
|
31063
|
+
/**
|
|
31064
|
+
* Presence runs inside the serialization chain, after any execution the
|
|
31065
|
+
* event raced with, so a room's presence write never interleaves with a
|
|
31066
|
+
* mutation. A reconnect inside the grace window is not a change.
|
|
31067
|
+
*/
|
|
31068
|
+
observeAttach(connection, reconnecting) {
|
|
31069
|
+
const pending = this.pendingDetach.get(connection.clientId);
|
|
31070
|
+
if (pending) {
|
|
31071
|
+
clearTimeout(pending);
|
|
31072
|
+
this.pendingDetach.delete(connection.clientId);
|
|
31073
|
+
return;
|
|
31074
|
+
}
|
|
31075
|
+
if (reconnecting) return;
|
|
31076
|
+
this.dispatchRoomPresence({ kind: "attached", connection, at: Date.now() });
|
|
31077
|
+
}
|
|
31078
|
+
observeDetach(connection) {
|
|
31079
|
+
if (this.liveSocketFor(connection.clientId)) return;
|
|
31080
|
+
const existing = this.pendingDetach.get(connection.clientId);
|
|
31081
|
+
if (existing) clearTimeout(existing);
|
|
31082
|
+
this.pendingDetach.set(
|
|
31083
|
+
connection.clientId,
|
|
31084
|
+
setTimeout(() => {
|
|
31085
|
+
this.pendingDetach.delete(connection.clientId);
|
|
31086
|
+
if (this.liveSocketFor(connection.clientId)) return;
|
|
31087
|
+
this.dispatchRoomPresence({ kind: "detached", connection, at: Date.now() });
|
|
31088
|
+
}, ROOM_PRESENCE_GRACE_MS)
|
|
31089
|
+
);
|
|
31090
|
+
}
|
|
31091
|
+
dispatchRoomPresence(event) {
|
|
31092
|
+
const run = this.executionChain.run(async () => {
|
|
31093
|
+
const room = await this.loadRoom();
|
|
31094
|
+
if (!room?.onPresence) return;
|
|
31095
|
+
const user = new UserStateImpl(this.roomUserId(), this.scopeHost(), { kind: "userRoom", name: this.roomKey ?? "room" });
|
|
31096
|
+
try {
|
|
31097
|
+
await room.onPresence(user, event);
|
|
31098
|
+
} catch (error) {
|
|
31099
|
+
console.error("[DurableUserHub] room presence handler failed:", error);
|
|
31100
|
+
}
|
|
31101
|
+
});
|
|
31102
|
+
this.ctx.waitUntil(run);
|
|
31103
|
+
}
|
|
31104
|
+
/** The hub is one object per user; its name is the user id. */
|
|
31105
|
+
roomUserId() {
|
|
31106
|
+
return this.ctx.id.name ?? this.ctx.id.toString();
|
|
31107
|
+
}
|
|
31108
|
+
async dispatchRoomFrame(ws, frame) {
|
|
31109
|
+
const connection = this.connectionOf(this.clientInfo.get(ws));
|
|
31110
|
+
if (!connection) return;
|
|
31111
|
+
await this.executionChain.run(async () => {
|
|
31112
|
+
const room = await this.loadRoom();
|
|
31113
|
+
if (!room) {
|
|
31114
|
+
this.sendRoomFrame(ws, JSON.stringify({ type: "room", id: frame.id, error: "no_room" }));
|
|
31115
|
+
return;
|
|
31116
|
+
}
|
|
31117
|
+
const user = new UserStateImpl(this.roomUserId(), this.scopeHost(), { kind: "userRoom", name: this.roomKey ?? "room" });
|
|
31118
|
+
try {
|
|
31119
|
+
await room.onFrame(user, connection, frame);
|
|
31120
|
+
} catch (error) {
|
|
31121
|
+
console.error("[DurableUserHub] room frame handler failed:", error);
|
|
31122
|
+
this.sendRoomFrame(ws, JSON.stringify({ type: "room", id: frame.id, error: "handler_failed" }));
|
|
31123
|
+
}
|
|
31124
|
+
});
|
|
31125
|
+
}
|
|
30874
31126
|
// ─── User scope: event stream (spec: Users §4) ─────────────────────────────
|
|
30875
31127
|
/** Idle sockets answer heartbeats at the edge without waking the object.
|
|
30876
31128
|
* Registered at accept time rather than in the constructor so a hub that
|
|
@@ -30908,8 +31160,9 @@ var DurableUserHub = class extends DurableObject {
|
|
|
30908
31160
|
return new Response(null, { status: 101, webSocket: client });
|
|
30909
31161
|
}
|
|
30910
31162
|
/**
|
|
30911
|
-
* Deliver an event frame to every socket attached to this user's stream
|
|
30912
|
-
* At-most-once, live-only (spec: Users §4) —
|
|
31163
|
+
* Deliver an event frame to every socket attached to this user's stream
|
|
31164
|
+
* and to every room socket. At-most-once, live-only (spec: Users §4) —
|
|
31165
|
+
* no history, no acks.
|
|
30913
31166
|
*/
|
|
30914
31167
|
broadcastUserEvent(userId, event, data) {
|
|
30915
31168
|
const frame = JSON.stringify({ event, data });
|
|
@@ -30921,6 +31174,10 @@ var DurableUserHub = class extends DurableObject {
|
|
|
30921
31174
|
} catch {
|
|
30922
31175
|
}
|
|
30923
31176
|
}
|
|
31177
|
+
for (const ws of Array.from(this.clientSockets)) {
|
|
31178
|
+
if (ws.readyState !== WebSocket.OPEN || !this.clientInfo.get(ws)?.clientId) continue;
|
|
31179
|
+
this.sendRoomFrame(ws, frame);
|
|
31180
|
+
}
|
|
30924
31181
|
}
|
|
30925
31182
|
// ─── User scope: serialized endpoint execution (spec: Users §2.1, §3) ─────
|
|
30926
31183
|
/** Per-user execution chain (spec: Users §2.1) — see users/serialize.ts. */
|
|
@@ -30931,6 +31188,8 @@ var DurableUserHub = class extends DurableObject {
|
|
|
30931
31188
|
setUserValue: (userId, key, value) => this.setUserValue(userId, key, value),
|
|
30932
31189
|
listUserValues: (userId, options) => this.listUserValues(userId, options ?? {}),
|
|
30933
31190
|
broadcastUserEvent: (userId, event, data) => this.broadcastUserEvent(userId, event, data),
|
|
31191
|
+
listRoomClients: () => this.listRoomClients(),
|
|
31192
|
+
sendToRoomClient: (_userId, clientId, frame) => this.sendToRoomClient(clientId, frame),
|
|
30934
31193
|
grantFetch: (userId, grant, req) => this.grantFetch(userId, grant, req),
|
|
30935
31194
|
env: this.env
|
|
30936
31195
|
};
|
|
@@ -30950,6 +31209,7 @@ var DurableUserHub = class extends DurableObject {
|
|
|
30950
31209
|
}
|
|
30951
31210
|
const module = loaded;
|
|
30952
31211
|
const handler = module.default ?? loaded;
|
|
31212
|
+
if (isUserRoom(handler)) return Response.json({ error: "Not found" }, { status: 404 });
|
|
30953
31213
|
if (!isUserEndpoint(handler)) {
|
|
30954
31214
|
return Response.json(
|
|
30955
31215
|
{ error: `${match.key} is not a defineUserEndpoint export` },
|
|
@@ -30993,6 +31253,7 @@ var DurableUserHub = class extends DurableObject {
|
|
|
30993
31253
|
const names = Object.keys(registry);
|
|
30994
31254
|
if (names.length === 0) return;
|
|
30995
31255
|
const run = async () => {
|
|
31256
|
+
if (hookType === "user_value_changed" && await this.roomOwnsKey(String(args[0]))) return;
|
|
30996
31257
|
const user = new UserStateImpl(userId, this.scopeHost(), {
|
|
30997
31258
|
kind: "userHook",
|
|
30998
31259
|
name: hookType
|
|
@@ -31017,8 +31278,9 @@ var DurableUserHub = class extends DurableObject {
|
|
|
31017
31278
|
this.ctx.waitUntil(run());
|
|
31018
31279
|
}
|
|
31019
31280
|
async webSocketClose(ws) {
|
|
31020
|
-
this.
|
|
31021
|
-
this.
|
|
31281
|
+
const connection = this.connectionOf(this.clientInfo.get(ws));
|
|
31282
|
+
this.forgetSocket(ws);
|
|
31283
|
+
if (connection) this.observeDetach(connection);
|
|
31022
31284
|
const attachment = ws.deserializeAttachment();
|
|
31023
31285
|
if (attachment?.type === "user_stream") {
|
|
31024
31286
|
this.dispatchUserHooks(String(attachment.userId), "user_client_detached", [
|
|
@@ -31147,7 +31409,7 @@ var DurableUserHub = class extends DurableObject {
|
|
|
31147
31409
|
caller: body.caller ?? { kind: "unknown", name: "unknown" }
|
|
31148
31410
|
});
|
|
31149
31411
|
}
|
|
31150
|
-
async webSocketMessage(ws,
|
|
31412
|
+
async webSocketMessage(ws, message) {
|
|
31151
31413
|
const attachment = ws.deserializeAttachment();
|
|
31152
31414
|
if (attachment?.type !== "hub_client") return;
|
|
31153
31415
|
if (!this.clientSockets.has(ws)) {
|
|
@@ -31155,9 +31417,29 @@ var DurableUserHub = class extends DurableObject {
|
|
|
31155
31417
|
this.clientInfo.set(ws, {
|
|
31156
31418
|
clientId: typeof attachment.clientId === "string" ? attachment.clientId : null,
|
|
31157
31419
|
clientName: typeof attachment.clientName === "string" ? attachment.clientName : null,
|
|
31158
|
-
clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null
|
|
31420
|
+
clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null,
|
|
31421
|
+
since: typeof attachment.since === "number" ? attachment.since : void 0
|
|
31159
31422
|
});
|
|
31160
31423
|
}
|
|
31424
|
+
if (!this.limitsFor(ws).rate.take()) {
|
|
31425
|
+
this.forgetSocket(ws);
|
|
31426
|
+
try {
|
|
31427
|
+
ws.close(ROOM_CLOSE_TRY_AGAIN, "Frame rate exceeded");
|
|
31428
|
+
} catch {
|
|
31429
|
+
}
|
|
31430
|
+
return;
|
|
31431
|
+
}
|
|
31432
|
+
const frame = parseRoomFrame(message);
|
|
31433
|
+
if (frame === "too_large") {
|
|
31434
|
+
this.forgetSocket(ws);
|
|
31435
|
+
try {
|
|
31436
|
+
ws.close(ROOM_CLOSE_TOO_LARGE, "Frame exceeds 1 MiB");
|
|
31437
|
+
} catch {
|
|
31438
|
+
}
|
|
31439
|
+
return;
|
|
31440
|
+
}
|
|
31441
|
+
if (frame === null || !this.clientInfo.get(ws)?.clientId) return;
|
|
31442
|
+
await this.dispatchRoomFrame(ws, frame);
|
|
31161
31443
|
}
|
|
31162
31444
|
async webSocketError(ws) {
|
|
31163
31445
|
await this.webSocketClose(ws);
|