@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/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { M as ModelDefinition, A as AgentDefinition } from './resolver-BMZAzKD3.js';
2
- export { m as AgentBuilderEnv, n as AgentBuilderEvent, 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, 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-BMZAzKD3.js';
1
+ import { M as ModelDefinition, A as AgentDefinition } from './resolver-DWuGxI4w.js';
2
+ export { m as AgentBuilderEnv, n as AgentBuilderEvent, 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, 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';
3
3
  export { AgentPluginOptions, agentbuilder, builder } from './plugin.js';
4
4
  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';
5
5
  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, ProviderConfigSlot, ProviderConfigSlots, ProviderConfigValueSource, ProviderDefinition, ProviderError, ProviderErrorCode, ProviderFactory, ProviderFactoryConfig, ProviderFinishReason, ProviderGeneratedImage, ProviderMessage, ProviderMessageContent, ProviderMethodOverrides, ModelCapabilities as ProviderModelCapabilities, ProviderOverride, ProviderOverrideContext, ProviderReasoningDetail, ProviderRequest, ProviderResponse, ProviderStreamChunk, ProviderSyncOverride, 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';
package/dist/index.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';
@@ -39449,6 +39449,12 @@ var UserStateImpl = class {
39449
39449
  emit(event, data) {
39450
39450
  this.host.broadcastUserEvent(this.userId, event, data);
39451
39451
  }
39452
+ async clients() {
39453
+ return this.host.listRoomClients(this.userId);
39454
+ }
39455
+ async send(clientId, frame) {
39456
+ return this.host.sendToRoomClient(this.userId, clientId, frame);
39457
+ }
39452
39458
  async listThreads(options) {
39453
39459
  const { threads, total } = await this.singleton().listThreads({
39454
39460
  user_id: this.userId,
@@ -39554,6 +39560,69 @@ function responseWithUpstreamTiming(response, name, durationMs) {
39554
39560
  // src/durable-objects/DurableUserHub.ts
39555
39561
  init_bridge_owner();
39556
39562
 
39563
+ // src/users/room.ts
39564
+ var ROOM_FRAME_MAX_BYTES = 1024 * 1024;
39565
+ var ROOM_FRAME_RATE_PER_SECOND = 200;
39566
+ var ROOM_FRAME_BURST = 400;
39567
+ var ROOM_SEND_BUDGET_BYTES = 8 * 1024 * 1024;
39568
+ var ROOM_SEND_BUDGET_WINDOW_MS = 1e4;
39569
+ var ROOM_PRESENCE_GRACE_MS = 3e4;
39570
+ var ROOM_CLOSE_SUPERSEDED = 4001;
39571
+ var ROOM_CLOSE_TOO_LARGE = 1009;
39572
+ var ROOM_CLOSE_TRY_AGAIN = 1013;
39573
+ function parseRoomFrame(message) {
39574
+ const size = typeof message === "string" ? message.length : message.byteLength;
39575
+ if (size > ROOM_FRAME_MAX_BYTES) return "too_large";
39576
+ if (typeof message !== "string") return null;
39577
+ let parsed;
39578
+ try {
39579
+ parsed = JSON.parse(message);
39580
+ } catch {
39581
+ return null;
39582
+ }
39583
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
39584
+ const frame = parsed;
39585
+ if (frame.type !== "room" || typeof frame.id !== "string" || frame.id.length === 0 || frame.id.length > 256) {
39586
+ return null;
39587
+ }
39588
+ return frame;
39589
+ }
39590
+ var RoomRateLimiter = class {
39591
+ constructor(ratePerSecond = ROOM_FRAME_RATE_PER_SECOND, burst = ROOM_FRAME_BURST, now = Date.now()) {
39592
+ this.ratePerSecond = ratePerSecond;
39593
+ this.burst = burst;
39594
+ this.tokens = burst;
39595
+ this.last = now;
39596
+ }
39597
+ tokens;
39598
+ last;
39599
+ take(now = Date.now()) {
39600
+ const elapsed = Math.max(0, now - this.last) / 1e3;
39601
+ this.tokens = Math.min(this.burst, this.tokens + elapsed * this.ratePerSecond);
39602
+ this.last = now;
39603
+ if (this.tokens < 1) return false;
39604
+ this.tokens -= 1;
39605
+ return true;
39606
+ }
39607
+ };
39608
+ var RoomSendBudget = class {
39609
+ constructor(budgetBytes = ROOM_SEND_BUDGET_BYTES, windowMs = ROOM_SEND_BUDGET_WINDOW_MS, now = Date.now()) {
39610
+ this.budgetBytes = budgetBytes;
39611
+ this.windowMs = windowMs;
39612
+ this.windowStart = now;
39613
+ }
39614
+ spent = 0;
39615
+ windowStart;
39616
+ charge(bytes, now = Date.now()) {
39617
+ if (now - this.windowStart >= this.windowMs) {
39618
+ this.windowStart = now;
39619
+ this.spent = 0;
39620
+ }
39621
+ this.spent += bytes;
39622
+ return this.spent <= this.budgetBytes;
39623
+ }
39624
+ };
39625
+
39557
39626
  // src/users/grants.ts
39558
39627
  var INSTANCE_GRANTS_MAX_BYTES = 64 * 1024;
39559
39628
  var INSTANCE_GRANTS_MAX_COUNT = 64;
@@ -39709,6 +39778,12 @@ var DurableUserHub = class extends DurableObject {
39709
39778
  clientSockets = /* @__PURE__ */ new Set();
39710
39779
  clientInfo = /* @__PURE__ */ new Map();
39711
39780
  migrated = false;
39781
+ /** Inbound frame rate and outbound byte budget per room socket. */
39782
+ roomLimits = /* @__PURE__ */ new Map();
39783
+ /** Detach notices held for the presence grace window, by client id. */
39784
+ pendingDetach = /* @__PURE__ */ new Map();
39785
+ /** The project's room definition, loaded once per instance. */
39786
+ roomDefinition = null;
39712
39787
  constructor(ctx, env) {
39713
39788
  super(ctx, env);
39714
39789
  ctx.blockConcurrencyWhile(async () => {
@@ -39721,7 +39796,8 @@ var DurableUserHub = class extends DurableObject {
39721
39796
  this.clientInfo.set(ws, {
39722
39797
  clientId: typeof attachment.clientId === "string" ? attachment.clientId : null,
39723
39798
  clientName: typeof attachment.clientName === "string" ? attachment.clientName : null,
39724
- clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null
39799
+ clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null,
39800
+ since: typeof attachment.since === "number" ? attachment.since : void 0
39725
39801
  });
39726
39802
  }
39727
39803
  }
@@ -39794,20 +39870,188 @@ var DurableUserHub = class extends DurableObject {
39794
39870
  if (request.headers.get("Upgrade") === "websocket") {
39795
39871
  const pair = new WebSocketPair();
39796
39872
  const [client, server] = [pair[0], pair[1]];
39797
- const info = parseBridgeClientParams(url).info;
39873
+ const info = { ...parseBridgeClientParams(url).info, since: Date.now() };
39798
39874
  this.ensureHeartbeatAutoResponse();
39799
39875
  this.ctx.acceptWebSocket(server);
39800
39876
  server.serializeAttachment({ type: "hub_client", ...info });
39877
+ const reconnecting = info.clientId !== null && this.supersedeClient(info.clientId, server);
39801
39878
  this.clientSockets.add(server);
39802
39879
  this.clientInfo.set(server, info);
39803
39880
  try {
39804
39881
  server.send(JSON.stringify({ type: "hub_ready" }));
39805
39882
  } catch {
39806
39883
  }
39884
+ if (info.clientId) {
39885
+ const connection = this.connectionOf(info);
39886
+ if (connection) this.observeAttach(connection, reconnecting);
39887
+ }
39807
39888
  return new Response(null, { status: 101, webSocket: client });
39808
39889
  }
39809
39890
  return new Response("Not found", { status: 404 });
39810
39891
  }
39892
+ // ─── Room connections (spec: Users rooms) ───────────────────────────────────
39893
+ connectionOf(info) {
39894
+ if (!info?.clientId) return null;
39895
+ return { clientId: info.clientId, clientKind: info.clientKind, since: info.since ?? 0 };
39896
+ }
39897
+ /** Close every other socket presenting `clientId`; true when one existed. */
39898
+ supersedeClient(clientId, keep) {
39899
+ let superseded = false;
39900
+ for (const ws of Array.from(this.clientSockets)) {
39901
+ if (ws === keep || this.clientInfo.get(ws)?.clientId !== clientId) continue;
39902
+ this.forgetSocket(ws);
39903
+ superseded = true;
39904
+ try {
39905
+ ws.send(JSON.stringify({ type: "superseded" }));
39906
+ } catch {
39907
+ }
39908
+ try {
39909
+ ws.close(ROOM_CLOSE_SUPERSEDED, "Superseded by a newer connection from this client");
39910
+ } catch {
39911
+ }
39912
+ }
39913
+ return superseded;
39914
+ }
39915
+ forgetSocket(ws) {
39916
+ this.clientSockets.delete(ws);
39917
+ this.clientInfo.delete(ws);
39918
+ this.roomLimits.delete(ws);
39919
+ }
39920
+ liveSocketFor(clientId) {
39921
+ for (const ws of this.clientSockets) {
39922
+ if (this.clientInfo.get(ws)?.clientId === clientId && ws.readyState === WebSocket.OPEN) return ws;
39923
+ }
39924
+ return null;
39925
+ }
39926
+ /** Live room connections, one per client id, oldest attachment first. */
39927
+ listRoomClients() {
39928
+ const byId = /* @__PURE__ */ new Map();
39929
+ for (const ws of this.clientSockets) {
39930
+ if (ws.readyState !== WebSocket.OPEN) continue;
39931
+ const connection = this.connectionOf(this.clientInfo.get(ws));
39932
+ if (connection) byId.set(connection.clientId, connection);
39933
+ }
39934
+ return Array.from(byId.values()).sort((a, b) => a.since - b.since);
39935
+ }
39936
+ /** Deliver one frame to the socket carrying `clientId`, within its send budget. */
39937
+ sendToRoomClient(clientId, frame) {
39938
+ const ws = this.liveSocketFor(clientId);
39939
+ if (!ws) return false;
39940
+ return this.sendRoomFrame(ws, JSON.stringify(frame));
39941
+ }
39942
+ sendRoomFrame(ws, payload) {
39943
+ const limits = this.limitsFor(ws);
39944
+ if (!limits.send.charge(payload.length)) {
39945
+ this.forgetSocket(ws);
39946
+ try {
39947
+ ws.close(ROOM_CLOSE_TRY_AGAIN, "Slow consumer: send budget exceeded");
39948
+ } catch {
39949
+ }
39950
+ return false;
39951
+ }
39952
+ try {
39953
+ ws.send(payload);
39954
+ return true;
39955
+ } catch {
39956
+ this.forgetSocket(ws);
39957
+ return false;
39958
+ }
39959
+ }
39960
+ limitsFor(ws) {
39961
+ let limits = this.roomLimits.get(ws);
39962
+ if (!limits) {
39963
+ limits = { rate: new RoomRateLimiter(), send: new RoomSendBudget() };
39964
+ this.roomLimits.set(ws, limits);
39965
+ }
39966
+ return limits;
39967
+ }
39968
+ /** Load the project's room definition from the users registry, once. */
39969
+ async loadRoom() {
39970
+ if (!this.roomDefinition) {
39971
+ this.roomDefinition = (async () => {
39972
+ const registry = this.userEndpoints();
39973
+ for (const key of Object.keys(registry)) {
39974
+ if (!/(^|\/)room$/.test(key)) continue;
39975
+ try {
39976
+ const loaded = await registry[key]();
39977
+ const candidate = loaded.default ?? loaded;
39978
+ if (isUserRoom(candidate)) return candidate;
39979
+ } catch {
39980
+ }
39981
+ }
39982
+ return null;
39983
+ })();
39984
+ }
39985
+ return this.roomDefinition;
39986
+ }
39987
+ /** Whether `key` is room-owned state, exempt from `user_value_changed`. */
39988
+ async roomOwnsKey(key) {
39989
+ const room = await this.loadRoom();
39990
+ return Boolean(room?.valuePrefix && key.startsWith(room.valuePrefix));
39991
+ }
39992
+ /**
39993
+ * Presence runs inside the serialization chain, after any execution the
39994
+ * event raced with, so a room's presence write never interleaves with a
39995
+ * mutation. A reconnect inside the grace window is not a change.
39996
+ */
39997
+ observeAttach(connection, reconnecting) {
39998
+ const pending = this.pendingDetach.get(connection.clientId);
39999
+ if (pending) {
40000
+ clearTimeout(pending);
40001
+ this.pendingDetach.delete(connection.clientId);
40002
+ return;
40003
+ }
40004
+ if (reconnecting) return;
40005
+ this.dispatchRoomPresence({ kind: "attached", connection, at: Date.now() });
40006
+ }
40007
+ observeDetach(connection) {
40008
+ if (this.liveSocketFor(connection.clientId)) return;
40009
+ const existing = this.pendingDetach.get(connection.clientId);
40010
+ if (existing) clearTimeout(existing);
40011
+ this.pendingDetach.set(
40012
+ connection.clientId,
40013
+ setTimeout(() => {
40014
+ this.pendingDetach.delete(connection.clientId);
40015
+ if (this.liveSocketFor(connection.clientId)) return;
40016
+ this.dispatchRoomPresence({ kind: "detached", connection, at: Date.now() });
40017
+ }, ROOM_PRESENCE_GRACE_MS)
40018
+ );
40019
+ }
40020
+ dispatchRoomPresence(event) {
40021
+ const run = this.executionChain.run(async () => {
40022
+ const room = await this.loadRoom();
40023
+ if (!room?.onPresence) return;
40024
+ const user = new UserStateImpl(this.roomUserId(), this.scopeHost(), { kind: "userRoom", name: "presence" });
40025
+ try {
40026
+ await room.onPresence(user, event);
40027
+ } catch (error) {
40028
+ console.error("[DurableUserHub] room presence handler failed:", error);
40029
+ }
40030
+ });
40031
+ this.ctx.waitUntil(run);
40032
+ }
40033
+ /** The hub is one object per user; its name is the user id. */
40034
+ roomUserId() {
40035
+ return this.ctx.id.name ?? this.ctx.id.toString();
40036
+ }
40037
+ async dispatchRoomFrame(ws, frame) {
40038
+ const connection = this.connectionOf(this.clientInfo.get(ws));
40039
+ if (!connection) return;
40040
+ await this.executionChain.run(async () => {
40041
+ const room = await this.loadRoom();
40042
+ if (!room) {
40043
+ this.sendRoomFrame(ws, JSON.stringify({ type: "room", id: frame.id, error: "no_room" }));
40044
+ return;
40045
+ }
40046
+ const user = new UserStateImpl(this.roomUserId(), this.scopeHost(), { kind: "userRoom", name: "frame" });
40047
+ try {
40048
+ await room.onFrame(user, connection, frame);
40049
+ } catch (error) {
40050
+ console.error("[DurableUserHub] room frame handler failed:", error);
40051
+ this.sendRoomFrame(ws, JSON.stringify({ type: "room", id: frame.id, error: "handler_failed" }));
40052
+ }
40053
+ });
40054
+ }
39811
40055
  // ─── User scope: event stream (spec: Users §4) ─────────────────────────────
39812
40056
  /** Idle sockets answer heartbeats at the edge without waking the object.
39813
40057
  * Registered at accept time rather than in the constructor so a hub that
@@ -39845,8 +40089,9 @@ var DurableUserHub = class extends DurableObject {
39845
40089
  return new Response(null, { status: 101, webSocket: client });
39846
40090
  }
39847
40091
  /**
39848
- * Deliver an event frame to every socket attached to this user's stream.
39849
- * At-most-once, live-only (spec: Users §4) — no history, no acks.
40092
+ * Deliver an event frame to every socket attached to this user's stream
40093
+ * and to every room socket. At-most-once, live-only (spec: Users §4) —
40094
+ * no history, no acks.
39850
40095
  */
39851
40096
  broadcastUserEvent(userId, event, data) {
39852
40097
  const frame = JSON.stringify({ event, data });
@@ -39858,6 +40103,10 @@ var DurableUserHub = class extends DurableObject {
39858
40103
  } catch {
39859
40104
  }
39860
40105
  }
40106
+ for (const ws of Array.from(this.clientSockets)) {
40107
+ if (ws.readyState !== WebSocket.OPEN || !this.clientInfo.get(ws)?.clientId) continue;
40108
+ this.sendRoomFrame(ws, frame);
40109
+ }
39861
40110
  }
39862
40111
  // ─── User scope: serialized endpoint execution (spec: Users §2.1, §3) ─────
39863
40112
  /** Per-user execution chain (spec: Users §2.1) — see users/serialize.ts. */
@@ -39868,6 +40117,8 @@ var DurableUserHub = class extends DurableObject {
39868
40117
  setUserValue: (userId, key, value) => this.setUserValue(userId, key, value),
39869
40118
  listUserValues: (userId, options) => this.listUserValues(userId, options ?? {}),
39870
40119
  broadcastUserEvent: (userId, event, data) => this.broadcastUserEvent(userId, event, data),
40120
+ listRoomClients: () => this.listRoomClients(),
40121
+ sendToRoomClient: (_userId, clientId, frame) => this.sendToRoomClient(clientId, frame),
39871
40122
  grantFetch: (userId, grant, req) => this.grantFetch(userId, grant, req),
39872
40123
  env: this.env
39873
40124
  };
@@ -39887,6 +40138,7 @@ var DurableUserHub = class extends DurableObject {
39887
40138
  }
39888
40139
  const module = loaded;
39889
40140
  const handler = module.default ?? loaded;
40141
+ if (isUserRoom(handler)) return Response.json({ error: "Not found" }, { status: 404 });
39890
40142
  if (!isUserEndpoint(handler)) {
39891
40143
  return Response.json(
39892
40144
  { error: `${match.key} is not a defineUserEndpoint export` },
@@ -39930,6 +40182,7 @@ var DurableUserHub = class extends DurableObject {
39930
40182
  const names = Object.keys(registry);
39931
40183
  if (names.length === 0) return;
39932
40184
  const run = async () => {
40185
+ if (hookType === "user_value_changed" && await this.roomOwnsKey(String(args[0]))) return;
39933
40186
  const user = new UserStateImpl(userId, this.scopeHost(), {
39934
40187
  kind: "userHook",
39935
40188
  name: hookType
@@ -39954,8 +40207,9 @@ var DurableUserHub = class extends DurableObject {
39954
40207
  this.ctx.waitUntil(run());
39955
40208
  }
39956
40209
  async webSocketClose(ws) {
39957
- this.clientSockets.delete(ws);
39958
- this.clientInfo.delete(ws);
40210
+ const connection = this.connectionOf(this.clientInfo.get(ws));
40211
+ this.forgetSocket(ws);
40212
+ if (connection) this.observeDetach(connection);
39959
40213
  const attachment = ws.deserializeAttachment();
39960
40214
  if (attachment?.type === "user_stream") {
39961
40215
  this.dispatchUserHooks(String(attachment.userId), "user_client_detached", [
@@ -40084,7 +40338,7 @@ var DurableUserHub = class extends DurableObject {
40084
40338
  caller: body.caller ?? { kind: "unknown", name: "unknown" }
40085
40339
  });
40086
40340
  }
40087
- async webSocketMessage(ws, _message) {
40341
+ async webSocketMessage(ws, message) {
40088
40342
  const attachment = ws.deserializeAttachment();
40089
40343
  if (attachment?.type !== "hub_client") return;
40090
40344
  if (!this.clientSockets.has(ws)) {
@@ -40092,9 +40346,29 @@ var DurableUserHub = class extends DurableObject {
40092
40346
  this.clientInfo.set(ws, {
40093
40347
  clientId: typeof attachment.clientId === "string" ? attachment.clientId : null,
40094
40348
  clientName: typeof attachment.clientName === "string" ? attachment.clientName : null,
40095
- clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null
40349
+ clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null,
40350
+ since: typeof attachment.since === "number" ? attachment.since : void 0
40096
40351
  });
40097
40352
  }
40353
+ if (!this.limitsFor(ws).rate.take()) {
40354
+ this.forgetSocket(ws);
40355
+ try {
40356
+ ws.close(ROOM_CLOSE_TRY_AGAIN, "Frame rate exceeded");
40357
+ } catch {
40358
+ }
40359
+ return;
40360
+ }
40361
+ const frame = parseRoomFrame(message);
40362
+ if (frame === "too_large") {
40363
+ this.forgetSocket(ws);
40364
+ try {
40365
+ ws.close(ROOM_CLOSE_TOO_LARGE, "Frame exceeds 1 MiB");
40366
+ } catch {
40367
+ }
40368
+ return;
40369
+ }
40370
+ if (frame === null || !this.clientInfo.get(ws)?.clientId) return;
40371
+ await this.dispatchRoomFrame(ws, frame);
40098
40372
  }
40099
40373
  async webSocketError(ws) {
40100
40374
  await this.webSocketClose(ws);