@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
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { M as ModelDefinition, A as AgentDefinition } from './resolver-
|
|
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-
|
|
1
|
+
import { M as ModelDefinition, A as AgentDefinition } from './resolver-kvVdxct2.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-kvVdxct2.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;
|
|
@@ -39676,6 +39745,9 @@ function callerAllowed(allow, caller) {
|
|
|
39676
39745
|
endpoint: allow.endpoints,
|
|
39677
39746
|
userEndpoint: allow.userEndpoints,
|
|
39678
39747
|
userHook: allow.userEndpoints,
|
|
39748
|
+
// A room execution is policy-named by its registry key; a policy may
|
|
39749
|
+
// list it under `userRooms` or alongside the endpoints it serves.
|
|
39750
|
+
userRoom: allow.userRooms ?? allow.userEndpoints,
|
|
39679
39751
|
agent: allow.agents
|
|
39680
39752
|
};
|
|
39681
39753
|
const list = lists[caller.kind];
|
|
@@ -39709,6 +39781,14 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39709
39781
|
clientSockets = /* @__PURE__ */ new Set();
|
|
39710
39782
|
clientInfo = /* @__PURE__ */ new Map();
|
|
39711
39783
|
migrated = false;
|
|
39784
|
+
/** Inbound frame rate and outbound byte budget per room socket. */
|
|
39785
|
+
roomLimits = /* @__PURE__ */ new Map();
|
|
39786
|
+
/** Detach notices held for the presence grace window, by client id. */
|
|
39787
|
+
pendingDetach = /* @__PURE__ */ new Map();
|
|
39788
|
+
/** The project's room definition, loaded once per instance, and its
|
|
39789
|
+
* registry key, which names room executions for grant policies. */
|
|
39790
|
+
roomDefinition = null;
|
|
39791
|
+
roomKey = null;
|
|
39712
39792
|
constructor(ctx, env) {
|
|
39713
39793
|
super(ctx, env);
|
|
39714
39794
|
ctx.blockConcurrencyWhile(async () => {
|
|
@@ -39721,7 +39801,8 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39721
39801
|
this.clientInfo.set(ws, {
|
|
39722
39802
|
clientId: typeof attachment.clientId === "string" ? attachment.clientId : null,
|
|
39723
39803
|
clientName: typeof attachment.clientName === "string" ? attachment.clientName : null,
|
|
39724
|
-
clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null
|
|
39804
|
+
clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null,
|
|
39805
|
+
since: typeof attachment.since === "number" ? attachment.since : void 0
|
|
39725
39806
|
});
|
|
39726
39807
|
}
|
|
39727
39808
|
}
|
|
@@ -39794,20 +39875,191 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39794
39875
|
if (request.headers.get("Upgrade") === "websocket") {
|
|
39795
39876
|
const pair = new WebSocketPair();
|
|
39796
39877
|
const [client, server] = [pair[0], pair[1]];
|
|
39797
|
-
const info = parseBridgeClientParams(url).info;
|
|
39878
|
+
const info = { ...parseBridgeClientParams(url).info, since: Date.now() };
|
|
39798
39879
|
this.ensureHeartbeatAutoResponse();
|
|
39799
39880
|
this.ctx.acceptWebSocket(server);
|
|
39800
39881
|
server.serializeAttachment({ type: "hub_client", ...info });
|
|
39882
|
+
const reconnecting = info.clientId !== null && this.supersedeClient(info.clientId, server);
|
|
39801
39883
|
this.clientSockets.add(server);
|
|
39802
39884
|
this.clientInfo.set(server, info);
|
|
39803
39885
|
try {
|
|
39804
39886
|
server.send(JSON.stringify({ type: "hub_ready" }));
|
|
39805
39887
|
} catch {
|
|
39806
39888
|
}
|
|
39889
|
+
if (info.clientId) {
|
|
39890
|
+
const connection = this.connectionOf(info);
|
|
39891
|
+
if (connection) this.observeAttach(connection, reconnecting);
|
|
39892
|
+
}
|
|
39807
39893
|
return new Response(null, { status: 101, webSocket: client });
|
|
39808
39894
|
}
|
|
39809
39895
|
return new Response("Not found", { status: 404 });
|
|
39810
39896
|
}
|
|
39897
|
+
// ─── Room connections (spec: Users rooms) ───────────────────────────────────
|
|
39898
|
+
connectionOf(info) {
|
|
39899
|
+
if (!info?.clientId) return null;
|
|
39900
|
+
return { clientId: info.clientId, clientKind: info.clientKind, since: info.since ?? 0 };
|
|
39901
|
+
}
|
|
39902
|
+
/** Close every other socket presenting `clientId`; true when one existed. */
|
|
39903
|
+
supersedeClient(clientId, keep) {
|
|
39904
|
+
let superseded = false;
|
|
39905
|
+
for (const ws of Array.from(this.clientSockets)) {
|
|
39906
|
+
if (ws === keep || this.clientInfo.get(ws)?.clientId !== clientId) continue;
|
|
39907
|
+
this.forgetSocket(ws);
|
|
39908
|
+
superseded = true;
|
|
39909
|
+
try {
|
|
39910
|
+
ws.send(JSON.stringify({ type: "superseded" }));
|
|
39911
|
+
} catch {
|
|
39912
|
+
}
|
|
39913
|
+
try {
|
|
39914
|
+
ws.close(ROOM_CLOSE_SUPERSEDED, "Superseded by a newer connection from this client");
|
|
39915
|
+
} catch {
|
|
39916
|
+
}
|
|
39917
|
+
}
|
|
39918
|
+
return superseded;
|
|
39919
|
+
}
|
|
39920
|
+
forgetSocket(ws) {
|
|
39921
|
+
this.clientSockets.delete(ws);
|
|
39922
|
+
this.clientInfo.delete(ws);
|
|
39923
|
+
this.roomLimits.delete(ws);
|
|
39924
|
+
}
|
|
39925
|
+
liveSocketFor(clientId) {
|
|
39926
|
+
for (const ws of this.clientSockets) {
|
|
39927
|
+
if (this.clientInfo.get(ws)?.clientId === clientId && ws.readyState === WebSocket.OPEN) return ws;
|
|
39928
|
+
}
|
|
39929
|
+
return null;
|
|
39930
|
+
}
|
|
39931
|
+
/** Live room connections, one per client id, oldest attachment first. */
|
|
39932
|
+
listRoomClients() {
|
|
39933
|
+
const byId = /* @__PURE__ */ new Map();
|
|
39934
|
+
for (const ws of this.clientSockets) {
|
|
39935
|
+
if (ws.readyState !== WebSocket.OPEN) continue;
|
|
39936
|
+
const connection = this.connectionOf(this.clientInfo.get(ws));
|
|
39937
|
+
if (connection) byId.set(connection.clientId, connection);
|
|
39938
|
+
}
|
|
39939
|
+
return Array.from(byId.values()).sort((a, b) => a.since - b.since);
|
|
39940
|
+
}
|
|
39941
|
+
/** Deliver one frame to the socket carrying `clientId`, within its send budget. */
|
|
39942
|
+
sendToRoomClient(clientId, frame) {
|
|
39943
|
+
const ws = this.liveSocketFor(clientId);
|
|
39944
|
+
if (!ws) return false;
|
|
39945
|
+
return this.sendRoomFrame(ws, JSON.stringify(frame));
|
|
39946
|
+
}
|
|
39947
|
+
sendRoomFrame(ws, payload) {
|
|
39948
|
+
const limits = this.limitsFor(ws);
|
|
39949
|
+
if (!limits.send.charge(payload.length)) {
|
|
39950
|
+
this.forgetSocket(ws);
|
|
39951
|
+
try {
|
|
39952
|
+
ws.close(ROOM_CLOSE_TRY_AGAIN, "Slow consumer: send budget exceeded");
|
|
39953
|
+
} catch {
|
|
39954
|
+
}
|
|
39955
|
+
return false;
|
|
39956
|
+
}
|
|
39957
|
+
try {
|
|
39958
|
+
ws.send(payload);
|
|
39959
|
+
return true;
|
|
39960
|
+
} catch {
|
|
39961
|
+
this.forgetSocket(ws);
|
|
39962
|
+
return false;
|
|
39963
|
+
}
|
|
39964
|
+
}
|
|
39965
|
+
limitsFor(ws) {
|
|
39966
|
+
let limits = this.roomLimits.get(ws);
|
|
39967
|
+
if (!limits) {
|
|
39968
|
+
limits = { rate: new RoomRateLimiter(), send: new RoomSendBudget() };
|
|
39969
|
+
this.roomLimits.set(ws, limits);
|
|
39970
|
+
}
|
|
39971
|
+
return limits;
|
|
39972
|
+
}
|
|
39973
|
+
/** Load the project's room definition from the users registry, once. */
|
|
39974
|
+
async loadRoom() {
|
|
39975
|
+
if (!this.roomDefinition) {
|
|
39976
|
+
this.roomDefinition = (async () => {
|
|
39977
|
+
const registry = this.userEndpoints();
|
|
39978
|
+
for (const key of Object.keys(registry)) {
|
|
39979
|
+
if (!/(^|\/)room$/.test(key)) continue;
|
|
39980
|
+
try {
|
|
39981
|
+
const loaded = await registry[key]();
|
|
39982
|
+
const candidate = loaded.default ?? loaded;
|
|
39983
|
+
if (isUserRoom(candidate)) {
|
|
39984
|
+
this.roomKey = key;
|
|
39985
|
+
return candidate;
|
|
39986
|
+
}
|
|
39987
|
+
} catch {
|
|
39988
|
+
}
|
|
39989
|
+
}
|
|
39990
|
+
return null;
|
|
39991
|
+
})();
|
|
39992
|
+
}
|
|
39993
|
+
return this.roomDefinition;
|
|
39994
|
+
}
|
|
39995
|
+
/** Whether `key` is room-owned state, exempt from `user_value_changed`. */
|
|
39996
|
+
async roomOwnsKey(key) {
|
|
39997
|
+
const room = await this.loadRoom();
|
|
39998
|
+
return Boolean(room?.valuePrefix && key.startsWith(room.valuePrefix));
|
|
39999
|
+
}
|
|
40000
|
+
/**
|
|
40001
|
+
* Presence runs inside the serialization chain, after any execution the
|
|
40002
|
+
* event raced with, so a room's presence write never interleaves with a
|
|
40003
|
+
* mutation. A reconnect inside the grace window is not a change.
|
|
40004
|
+
*/
|
|
40005
|
+
observeAttach(connection, reconnecting) {
|
|
40006
|
+
const pending = this.pendingDetach.get(connection.clientId);
|
|
40007
|
+
if (pending) {
|
|
40008
|
+
clearTimeout(pending);
|
|
40009
|
+
this.pendingDetach.delete(connection.clientId);
|
|
40010
|
+
return;
|
|
40011
|
+
}
|
|
40012
|
+
if (reconnecting) return;
|
|
40013
|
+
this.dispatchRoomPresence({ kind: "attached", connection, at: Date.now() });
|
|
40014
|
+
}
|
|
40015
|
+
observeDetach(connection) {
|
|
40016
|
+
if (this.liveSocketFor(connection.clientId)) return;
|
|
40017
|
+
const existing = this.pendingDetach.get(connection.clientId);
|
|
40018
|
+
if (existing) clearTimeout(existing);
|
|
40019
|
+
this.pendingDetach.set(
|
|
40020
|
+
connection.clientId,
|
|
40021
|
+
setTimeout(() => {
|
|
40022
|
+
this.pendingDetach.delete(connection.clientId);
|
|
40023
|
+
if (this.liveSocketFor(connection.clientId)) return;
|
|
40024
|
+
this.dispatchRoomPresence({ kind: "detached", connection, at: Date.now() });
|
|
40025
|
+
}, ROOM_PRESENCE_GRACE_MS)
|
|
40026
|
+
);
|
|
40027
|
+
}
|
|
40028
|
+
dispatchRoomPresence(event) {
|
|
40029
|
+
const run = this.executionChain.run(async () => {
|
|
40030
|
+
const room = await this.loadRoom();
|
|
40031
|
+
if (!room?.onPresence) return;
|
|
40032
|
+
const user = new UserStateImpl(this.roomUserId(), this.scopeHost(), { kind: "userRoom", name: this.roomKey ?? "room" });
|
|
40033
|
+
try {
|
|
40034
|
+
await room.onPresence(user, event);
|
|
40035
|
+
} catch (error) {
|
|
40036
|
+
console.error("[DurableUserHub] room presence handler failed:", error);
|
|
40037
|
+
}
|
|
40038
|
+
});
|
|
40039
|
+
this.ctx.waitUntil(run);
|
|
40040
|
+
}
|
|
40041
|
+
/** The hub is one object per user; its name is the user id. */
|
|
40042
|
+
roomUserId() {
|
|
40043
|
+
return this.ctx.id.name ?? this.ctx.id.toString();
|
|
40044
|
+
}
|
|
40045
|
+
async dispatchRoomFrame(ws, frame) {
|
|
40046
|
+
const connection = this.connectionOf(this.clientInfo.get(ws));
|
|
40047
|
+
if (!connection) return;
|
|
40048
|
+
await this.executionChain.run(async () => {
|
|
40049
|
+
const room = await this.loadRoom();
|
|
40050
|
+
if (!room) {
|
|
40051
|
+
this.sendRoomFrame(ws, JSON.stringify({ type: "room", id: frame.id, error: "no_room" }));
|
|
40052
|
+
return;
|
|
40053
|
+
}
|
|
40054
|
+
const user = new UserStateImpl(this.roomUserId(), this.scopeHost(), { kind: "userRoom", name: this.roomKey ?? "room" });
|
|
40055
|
+
try {
|
|
40056
|
+
await room.onFrame(user, connection, frame);
|
|
40057
|
+
} catch (error) {
|
|
40058
|
+
console.error("[DurableUserHub] room frame handler failed:", error);
|
|
40059
|
+
this.sendRoomFrame(ws, JSON.stringify({ type: "room", id: frame.id, error: "handler_failed" }));
|
|
40060
|
+
}
|
|
40061
|
+
});
|
|
40062
|
+
}
|
|
39811
40063
|
// ─── User scope: event stream (spec: Users §4) ─────────────────────────────
|
|
39812
40064
|
/** Idle sockets answer heartbeats at the edge without waking the object.
|
|
39813
40065
|
* Registered at accept time rather than in the constructor so a hub that
|
|
@@ -39845,8 +40097,9 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39845
40097
|
return new Response(null, { status: 101, webSocket: client });
|
|
39846
40098
|
}
|
|
39847
40099
|
/**
|
|
39848
|
-
* Deliver an event frame to every socket attached to this user's stream
|
|
39849
|
-
* At-most-once, live-only (spec: Users §4) —
|
|
40100
|
+
* Deliver an event frame to every socket attached to this user's stream
|
|
40101
|
+
* and to every room socket. At-most-once, live-only (spec: Users §4) —
|
|
40102
|
+
* no history, no acks.
|
|
39850
40103
|
*/
|
|
39851
40104
|
broadcastUserEvent(userId, event, data) {
|
|
39852
40105
|
const frame = JSON.stringify({ event, data });
|
|
@@ -39858,6 +40111,10 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39858
40111
|
} catch {
|
|
39859
40112
|
}
|
|
39860
40113
|
}
|
|
40114
|
+
for (const ws of Array.from(this.clientSockets)) {
|
|
40115
|
+
if (ws.readyState !== WebSocket.OPEN || !this.clientInfo.get(ws)?.clientId) continue;
|
|
40116
|
+
this.sendRoomFrame(ws, frame);
|
|
40117
|
+
}
|
|
39861
40118
|
}
|
|
39862
40119
|
// ─── User scope: serialized endpoint execution (spec: Users §2.1, §3) ─────
|
|
39863
40120
|
/** Per-user execution chain (spec: Users §2.1) — see users/serialize.ts. */
|
|
@@ -39868,6 +40125,8 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39868
40125
|
setUserValue: (userId, key, value) => this.setUserValue(userId, key, value),
|
|
39869
40126
|
listUserValues: (userId, options) => this.listUserValues(userId, options ?? {}),
|
|
39870
40127
|
broadcastUserEvent: (userId, event, data) => this.broadcastUserEvent(userId, event, data),
|
|
40128
|
+
listRoomClients: () => this.listRoomClients(),
|
|
40129
|
+
sendToRoomClient: (_userId, clientId, frame) => this.sendToRoomClient(clientId, frame),
|
|
39871
40130
|
grantFetch: (userId, grant, req) => this.grantFetch(userId, grant, req),
|
|
39872
40131
|
env: this.env
|
|
39873
40132
|
};
|
|
@@ -39887,6 +40146,7 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39887
40146
|
}
|
|
39888
40147
|
const module = loaded;
|
|
39889
40148
|
const handler = module.default ?? loaded;
|
|
40149
|
+
if (isUserRoom(handler)) return Response.json({ error: "Not found" }, { status: 404 });
|
|
39890
40150
|
if (!isUserEndpoint(handler)) {
|
|
39891
40151
|
return Response.json(
|
|
39892
40152
|
{ error: `${match.key} is not a defineUserEndpoint export` },
|
|
@@ -39930,6 +40190,7 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39930
40190
|
const names = Object.keys(registry);
|
|
39931
40191
|
if (names.length === 0) return;
|
|
39932
40192
|
const run = async () => {
|
|
40193
|
+
if (hookType === "user_value_changed" && await this.roomOwnsKey(String(args[0]))) return;
|
|
39933
40194
|
const user = new UserStateImpl(userId, this.scopeHost(), {
|
|
39934
40195
|
kind: "userHook",
|
|
39935
40196
|
name: hookType
|
|
@@ -39954,8 +40215,9 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39954
40215
|
this.ctx.waitUntil(run());
|
|
39955
40216
|
}
|
|
39956
40217
|
async webSocketClose(ws) {
|
|
39957
|
-
this.
|
|
39958
|
-
this.
|
|
40218
|
+
const connection = this.connectionOf(this.clientInfo.get(ws));
|
|
40219
|
+
this.forgetSocket(ws);
|
|
40220
|
+
if (connection) this.observeDetach(connection);
|
|
39959
40221
|
const attachment = ws.deserializeAttachment();
|
|
39960
40222
|
if (attachment?.type === "user_stream") {
|
|
39961
40223
|
this.dispatchUserHooks(String(attachment.userId), "user_client_detached", [
|
|
@@ -40084,7 +40346,7 @@ var DurableUserHub = class extends DurableObject {
|
|
|
40084
40346
|
caller: body.caller ?? { kind: "unknown", name: "unknown" }
|
|
40085
40347
|
});
|
|
40086
40348
|
}
|
|
40087
|
-
async webSocketMessage(ws,
|
|
40349
|
+
async webSocketMessage(ws, message) {
|
|
40088
40350
|
const attachment = ws.deserializeAttachment();
|
|
40089
40351
|
if (attachment?.type !== "hub_client") return;
|
|
40090
40352
|
if (!this.clientSockets.has(ws)) {
|
|
@@ -40092,9 +40354,29 @@ var DurableUserHub = class extends DurableObject {
|
|
|
40092
40354
|
this.clientInfo.set(ws, {
|
|
40093
40355
|
clientId: typeof attachment.clientId === "string" ? attachment.clientId : null,
|
|
40094
40356
|
clientName: typeof attachment.clientName === "string" ? attachment.clientName : null,
|
|
40095
|
-
clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null
|
|
40357
|
+
clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null,
|
|
40358
|
+
since: typeof attachment.since === "number" ? attachment.since : void 0
|
|
40096
40359
|
});
|
|
40097
40360
|
}
|
|
40361
|
+
if (!this.limitsFor(ws).rate.take()) {
|
|
40362
|
+
this.forgetSocket(ws);
|
|
40363
|
+
try {
|
|
40364
|
+
ws.close(ROOM_CLOSE_TRY_AGAIN, "Frame rate exceeded");
|
|
40365
|
+
} catch {
|
|
40366
|
+
}
|
|
40367
|
+
return;
|
|
40368
|
+
}
|
|
40369
|
+
const frame = parseRoomFrame(message);
|
|
40370
|
+
if (frame === "too_large") {
|
|
40371
|
+
this.forgetSocket(ws);
|
|
40372
|
+
try {
|
|
40373
|
+
ws.close(ROOM_CLOSE_TOO_LARGE, "Frame exceeds 1 MiB");
|
|
40374
|
+
} catch {
|
|
40375
|
+
}
|
|
40376
|
+
return;
|
|
40377
|
+
}
|
|
40378
|
+
if (frame === null || !this.clientInfo.get(ws)?.clientId) return;
|
|
40379
|
+
await this.dispatchRoomFrame(ws, frame);
|
|
40098
40380
|
}
|
|
40099
40381
|
async webSocketError(ws) {
|
|
40100
40382
|
await this.webSocketClose(ws);
|