@tier0/sdk-v2 0.7.2 → 0.9.0
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/NOTICE.md +3 -1
- package/README.md +48 -10
- package/dist/cjs/capabilities.d.ts +8 -17
- package/dist/cjs/capabilities.js +14 -27
- package/dist/cjs/capabilities.js.map +1 -1
- package/dist/cjs/client.d.ts +524 -3
- package/dist/cjs/client.js +29 -6
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/cloud-transport.d.ts +4 -2
- package/dist/cjs/cloud-transport.js +27 -18
- package/dist/cjs/cloud-transport.js.map +1 -1
- package/dist/cjs/data/bootstrap.js +5 -5
- package/dist/cjs/data/bootstrap.js.map +1 -1
- package/dist/cjs/data/define.d.ts +6 -0
- package/dist/cjs/data/define.js +11 -1
- package/dist/cjs/data/define.js.map +1 -1
- package/dist/cjs/data/schema.d.ts +1 -0
- package/dist/cjs/data/schema.js +7 -0
- package/dist/cjs/data/schema.js.map +1 -1
- package/dist/cjs/files.d.ts +26 -6
- package/dist/cjs/files.js +151 -28
- package/dist/cjs/files.js.map +1 -1
- package/dist/cjs/index.d.ts +2 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/openapi/api.generated.d.ts +1 -1
- package/dist/cjs/openapi/codecs.generated.d.ts +30 -161
- package/dist/cjs/openapi/codecs.generated.js +1 -1
- package/dist/cjs/openapi/codecs.generated.js.map +1 -1
- package/dist/cjs/openapi/types.generated.d.ts +33 -85
- package/dist/cjs/openapi/types.generated.js.map +1 -1
- package/dist/cjs/runtime.d.ts +13 -0
- package/dist/cjs/runtime.js +51 -0
- package/dist/cjs/runtime.js.map +1 -0
- package/dist/cjs/streams/capabilities.d.ts +8 -0
- package/dist/cjs/streams/capabilities.js +19 -0
- package/dist/cjs/streams/capabilities.js.map +1 -0
- package/dist/cjs/streams/contract.generated.d.ts +802 -0
- package/dist/cjs/streams/contract.generated.js +7 -0
- package/dist/cjs/streams/contract.generated.js.map +1 -0
- package/dist/cjs/streams/index.d.ts +3 -0
- package/dist/cjs/streams/index.js +20 -0
- package/dist/cjs/streams/index.js.map +1 -0
- package/dist/cjs/streams/messages.d.ts +42 -0
- package/dist/cjs/streams/messages.js +514 -0
- package/dist/cjs/streams/messages.js.map +1 -0
- package/dist/cjs/streams/requests.d.ts +93 -0
- package/dist/cjs/streams/requests.js +167 -0
- package/dist/cjs/streams/requests.js.map +1 -0
- package/dist/cjs/streams/transport.d.ts +16 -0
- package/dist/cjs/streams/transport.js +24 -0
- package/dist/cjs/streams/transport.js.map +1 -0
- package/dist/cjs/uns-v2/generated.d.ts +88 -22
- package/dist/cjs/uns-v2/generated.js +43 -8
- package/dist/cjs/uns-v2/generated.js.map +1 -1
- package/dist/esm/capabilities.d.ts +8 -17
- package/dist/esm/capabilities.js +14 -27
- package/dist/esm/capabilities.js.map +1 -1
- package/dist/esm/client.d.ts +524 -3
- package/dist/esm/client.js +27 -5
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/cloud-transport.d.ts +4 -2
- package/dist/esm/cloud-transport.js +27 -18
- package/dist/esm/cloud-transport.js.map +1 -1
- package/dist/esm/data/bootstrap.js +5 -5
- package/dist/esm/data/bootstrap.js.map +1 -1
- package/dist/esm/data/define.d.ts +6 -0
- package/dist/esm/data/define.js +9 -0
- package/dist/esm/data/define.js.map +1 -1
- package/dist/esm/data/schema.d.ts +1 -0
- package/dist/esm/data/schema.js +8 -1
- package/dist/esm/data/schema.js.map +1 -1
- package/dist/esm/files.d.ts +26 -6
- package/dist/esm/files.js +151 -28
- package/dist/esm/files.js.map +1 -1
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/openapi/api.generated.d.ts +1 -1
- package/dist/esm/openapi/codecs.generated.d.ts +30 -161
- package/dist/esm/openapi/codecs.generated.js +1 -1
- package/dist/esm/openapi/codecs.generated.js.map +1 -1
- package/dist/esm/openapi/types.generated.d.ts +33 -85
- package/dist/esm/openapi/types.generated.js.map +1 -1
- package/dist/esm/runtime.d.ts +13 -0
- package/dist/esm/runtime.js +46 -0
- package/dist/esm/runtime.js.map +1 -0
- package/dist/esm/streams/capabilities.d.ts +8 -0
- package/dist/esm/streams/capabilities.js +15 -0
- package/dist/esm/streams/capabilities.js.map +1 -0
- package/dist/esm/streams/contract.generated.d.ts +802 -0
- package/dist/esm/streams/contract.generated.js +6 -0
- package/dist/esm/streams/contract.generated.js.map +1 -0
- package/dist/esm/streams/index.d.ts +3 -0
- package/dist/esm/streams/index.js +4 -0
- package/dist/esm/streams/index.js.map +1 -0
- package/dist/esm/streams/messages.d.ts +42 -0
- package/dist/esm/streams/messages.js +475 -0
- package/dist/esm/streams/messages.js.map +1 -0
- package/dist/esm/streams/requests.d.ts +93 -0
- package/dist/esm/streams/requests.js +161 -0
- package/dist/esm/streams/requests.js.map +1 -0
- package/dist/esm/streams/transport.d.ts +16 -0
- package/dist/esm/streams/transport.js +20 -0
- package/dist/esm/streams/transport.js.map +1 -0
- package/dist/esm/uns-v2/generated.d.ts +88 -22
- package/dist/esm/uns-v2/generated.js +43 -8
- package/dist/esm/uns-v2/generated.js.map +1 -1
- package/package.json +46 -12
- package/skills/tier0-sdk-v2/SKILL.md +3 -2
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/SKILL.md +3 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/consistency.md +3 -3
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/errors.md +1 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/modeling.md +6 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/read-write.md +3 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/schema-evolution.md +6 -4
- package/skills/tier0-sdk-v2/tier0-sdk-v2-files/references/multipart.md +23 -8
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/SKILL.md +3 -3
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/create.md +9 -3
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/state.md +1 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/update.md +1 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/SKILL.md +2 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/create.md +19 -4
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/deploy.md +1 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/read.md +2 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/update.md +1 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-streams/SKILL.md +67 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-system/SKILL.md +5 -9
- package/skills/tier0-sdk-v2/tier0-sdk-v2-system/references/info.md +1 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/create.md +1 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/namespace.md +4 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.generated.js","sourceRoot":"","sources":["../../../src/streams/contract.generated.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * This file was auto-generated by openapi-typescript.\n * Do not make direct changes to the file.\n */\n\nexport interface paths {\n \"/openapi/v2/stream-requests/discover\": {\n parameters: {\n query?: never;\n header?: never;\n path?: never;\n cookie?: never;\n };\n get?: never;\n put?: never;\n /**\n * Discover Stream requests\n * @description 发现可见Stream 请求合同,只公开Stream 请求合同,不返回内部运行绑定\n */\n post: operations[\"PlatformV2StreamRequestDiscover\"];\n delete?: never;\n options?: never;\n head?: never;\n patch?: never;\n trace?: never;\n };\n \"/openapi/v2/stream-requests/{requestCode}/describe\": {\n parameters: {\n query?: never;\n header?: never;\n path?: never;\n cookie?: never;\n };\n get?: never;\n put?: never;\n /**\n * Describe Stream request\n * @description 读取公开Stream 请求合同,只公开Stream 请求合同,不返回内部运行绑定\n */\n post: operations[\"PlatformV2StreamRequestDescribe\"];\n delete?: never;\n options?: never;\n head?: never;\n patch?: never;\n trace?: never;\n };\n \"/openapi/v2/stream-requests/{requestCode}/invoke\": {\n parameters: {\n query?: never;\n header?: never;\n path?: never;\n cookie?: never;\n };\n get?: never;\n put?: never;\n /**\n * Invoke Stream request\n * @description 按公开合同调用Stream 请求;原始 body 为有效 UTF-8,最多 1MiB 字节;可选 idempotencyKey 解码后最多 128 UTF-8 字节,是否必填由冻结合同决定。返回 code、contractVersion、result、runID、status、replayed(boolean),不返回内部运行绑定。结果未知返回 HTTP 504,errorCode=STREAM_REQUEST_RESULT_UNKNOWN;未知内部错误返回 HTTP 500,errorCode=STREAM_REQUEST_INTERNAL_ERROR,message 不泄漏内部异常\n */\n post: operations[\"PlatformV2StreamRequestInvoke\"];\n delete?: never;\n options?: never;\n head?: never;\n patch?: never;\n trace?: never;\n };\n \"/openapi/v2/stream-messages/connect\": {\n parameters: {\n query?: never;\n header?: never;\n path?: never;\n cookie?: never;\n };\n get?: never;\n put?: never;\n /** Connect Stream messages */\n post: operations[\"PlatformV2StreamMessagesConnect\"];\n delete?: never;\n options?: never;\n head?: never;\n patch?: never;\n trace?: never;\n };\n \"/openapi/v2/capabilities/discover\": {\n parameters: {\n query?: never;\n header?: never;\n path?: never;\n cookie?: never;\n };\n get?: never;\n put?: never;\n /**\n * 发现当前 Workspace 的公开能力与版本合同\n * @description 需要有效 Workspace API Key,Body 仅接受空 JSON 对象,最大 4KiB UTF-8 字节,不接受查询参数或 Workspace 覆盖。成功响应直接为 CapabilityDiscoverV2Resp,不包裹 code/msg/data。能力由默认关闭的公开发布开关、Workspace FeatureGate、Key 显式授权共同裁决;Namespace 与 Stream request 能力仅报告当前发布与授权,运行可用性保持 readiness_unknown,不触发运行时探针。Source/Stream 保留独立操作前提的证据。state=ready 不替代具体资源授权或实际运行回执。合同版本 2.0 使用 state、reasonCode、operations,不再返回重复布尔状态。实际操作由后端执行安全检查,失败返回真实错误。响应允许客户端忽略未知扩展字段;版本标签不表示设备 ACK 或 npm 包版本。错误直接返回 CapabilityErrorResp 和真实 HTTP 状态,不泄漏内部异常。\n */\n post: operations[\"PlatformV2CapabilityDiscoverV2\"];\n delete?: never;\n options?: never;\n head?: never;\n patch?: never;\n trace?: never;\n };\n}\nexport type webhooks = Record<string, never>;\nexport interface components {\n schemas: {\n StreamRequestDiscoverReq: {\n /** @description 缺省为 1 */\n page?: number;\n search?: string;\n /** @description 缺省为 20 */\n size?: number;\n };\n StreamRequestDiscoverResp: {\n list: components[\"schemas\"][\"PublicStreamRequest\"][];\n total: number;\n };\n PublicStreamRequest: {\n code: string;\n contractVersion: string;\n description: string;\n idempotency: string;\n inputSchema: {\n [key: string]: unknown;\n };\n name: string;\n outputSchema: {\n [key: string]: unknown;\n };\n sideEffect: string;\n status: string;\n timeoutMs: number;\n };\n NotificationError: {\n errorCode: string;\n message: string;\n };\n StreamRequestInvokeReq: {\n /** @description 解码后的 UTF-8 字节数最多 128;是否必填由冻结合同决定 */\n idempotencyKey?: string;\n input: unknown;\n };\n StreamRequestInvokeResp: {\n code: string;\n /** @description 实际执行使用的冻结合同版本 */\n contractVersion: string;\n /** @description true=幂等重放,false=首次执行 */\n replayed: boolean;\n /** @description 合同定义的 JSON 结果,允许 null */\n result: unknown;\n /** @description 服务端返回的执行标识,不透明字符串 */\n runID: string;\n /** @description 服务端返回的终态,非 accepted/queued/pending/running */\n status: string;\n };\n StreamMessagesConnectReq: {\n appId: string;\n clientPublicKey: string;\n topics: components[\"schemas\"][\"StreamMessageTopicPermission\"][];\n };\n StreamMessageTopicPermission: {\n publish: boolean;\n subscribe: boolean;\n topic: string;\n };\n StreamMessagesConnectResp: {\n contractVersion: string;\n expiresAt: string;\n maxPayloadBytes: number;\n permissions: components[\"schemas\"][\"StreamMessageGrantedTopic\"][];\n url: string;\n userJwt: string;\n };\n StreamMessageGrantedTopic: {\n publishSubject: string;\n subscribeSubject: string;\n topic: string;\n };\n /**\n * Tier0 capability discovery 2.0\n * @description OpenAPI 3.0 wire schema. Array contains/cardinality annotations and cross-field proof ordering require the shared JSON Schema plus SDK semantic checks; this projection alone is not full contract acceptance.\n */\n CapabilityDiscoverV2Resp: {\n /** @enum {unknown} */\n product: \"cloud\" | \"enterprise\";\n productVersion?: string;\n /** @enum {unknown} */\n contractVersion: \"2.0\";\n apiContractVersions: string[];\n sdkContractVersions: string[];\n capabilities: components[\"schemas\"][\"CapabilityDiscoverycapability\"][] & (unknown & unknown & unknown & unknown & unknown);\n deprecations: {\n contract: string;\n replacement?: string;\n sunsetAt?: string;\n }[];\n };\n /** @description Unknown capability codes are ignored; only recognized codes use the frozen assessment contract. */\n CapabilityDiscoverycapability: {\n code: string;\n } & (unknown | components[\"schemas\"][\"CapabilityDiscoveryknownCapability\"]);\n /** @description State reports publication and assessed prerequisites only; ready never grants resource access or confirms an actual execution. */\n CapabilityDiscoveryknownCapability: {\n /** @enum {unknown} */\n state: \"disabled\" | \"not_supported\" | \"unavailable\" | \"readiness_unknown\" | \"ready\";\n reasonCode: string;\n /** Format: date-time */\n checkedAt?: string;\n /**\n * Format: date-time\n * @description Proof expiry must follow checkedAt. Cross-field chronological ordering is a semantic constraint checked by consumers in addition to this structural JSON Schema.\n */\n validUntil?: string;\n retryAfterMs?: number;\n code: string;\n contractVersions: string[];\n operations?: components[\"schemas\"][\"CapabilityDiscoveryoperation\"][];\n } & ((unknown | {\n /** @enum {unknown} */\n state?: \"ready\";\n checkedAt: unknown;\n }) & (unknown | ({\n operations?: ((unknown | components[\"schemas\"][\"CapabilityDiscoveryknownOperation\"]) & (unknown | {\n /** @enum {unknown} */\n requiresResourceCheck?: true;\n }))[] & (unknown & unknown & unknown & unknown & unknown & unknown);\n } & (unknown | {\n operations?: (unknown | {\n /** @enum {unknown} */\n state?: \"disabled\";\n })[];\n }))) & (unknown | ({\n operations?: ((unknown | components[\"schemas\"][\"CapabilityDiscoveryknownOperation\"]) & (unknown | {\n /** @enum {unknown} */\n requiresResourceCheck?: true;\n }))[] & (unknown & unknown & unknown & unknown & unknown & unknown);\n } & (unknown | {\n operations?: (unknown | {\n /** @enum {unknown} */\n state?: \"disabled\";\n })[];\n }))) & (unknown | {\n contractVersions?: unknown[];\n }) & (unknown | {\n contractVersions?: unknown[] & (unknown & unknown);\n }));\n /** @description Unknown operation names are ignored within their parent capability; known names are validated by that capability. */\n CapabilityDiscoveryoperation: {\n operation: string;\n };\n /** @description State reports publication and assessed prerequisites only; ready never grants resource access or confirms an actual execution. */\n CapabilityDiscoveryknownOperation: {\n /** @enum {unknown} */\n state: \"disabled\" | \"not_supported\" | \"unavailable\" | \"readiness_unknown\" | \"ready\";\n reasonCode: string;\n /** Format: date-time */\n checkedAt?: string;\n /**\n * Format: date-time\n * @description Proof expiry must follow checkedAt. Cross-field chronological ordering is a semantic constraint checked by consumers in addition to this structural JSON Schema.\n */\n validUntil?: string;\n retryAfterMs?: number;\n operation: string;\n requiresResourceCheck: boolean;\n } & (unknown | {\n /** @enum {unknown} */\n state?: \"ready\";\n checkedAt: unknown;\n });\n CapabilityErrorResp: {\n /** @description INVALID_CREDENTIAL / CAPABILITY_INVALID_REQUEST / CAPABILITY_FORBIDDEN / CAPABILITY_UNAVAILABLE / CAPABILITY_INTERNAL_ERROR */\n errorCode: string;\n /** @description 固定公开消息,不透传内部异常 */\n message: string;\n };\n };\n responses: never;\n parameters: never;\n requestBodies: never;\n headers: never;\n pathItems: never;\n}\nexport type $defs = Record<string, never>;\nexport interface operations {\n PlatformV2StreamRequestDiscover: {\n parameters: {\n query?: never;\n header?: never;\n path?: never;\n cookie?: never;\n };\n requestBody: {\n content: {\n \"application/json\": components[\"schemas\"][\"StreamRequestDiscoverReq\"];\n };\n };\n responses: {\n 200: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"StreamRequestDiscoverResp\"];\n };\n };\n /** @description Platform operation failed. */\n 400: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 401: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 403: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 404: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 409: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 422: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 429: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 500: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 503: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 504: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n };\n };\n PlatformV2StreamRequestDescribe: {\n parameters: {\n query?: never;\n header?: never;\n path: {\n requestCode: string;\n };\n cookie?: never;\n };\n requestBody?: never;\n responses: {\n 200: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"PublicStreamRequest\"];\n };\n };\n /** @description Platform operation failed. */\n 400: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 401: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 403: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 404: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 409: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 422: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 429: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 500: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 503: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 504: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n };\n };\n PlatformV2StreamRequestInvoke: {\n parameters: {\n query?: never;\n header?: never;\n path: {\n requestCode: string;\n };\n cookie?: never;\n };\n requestBody: {\n content: {\n \"application/json\": components[\"schemas\"][\"StreamRequestInvokeReq\"];\n };\n };\n responses: {\n 200: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"StreamRequestInvokeResp\"];\n };\n };\n /** @description Platform operation failed. */\n 400: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 401: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 403: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 404: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 409: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 422: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 429: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 500: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 503: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 504: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n };\n };\n PlatformV2StreamMessagesConnect: {\n parameters: {\n query?: never;\n header?: never;\n path?: never;\n cookie?: never;\n };\n requestBody: {\n content: {\n \"application/json\": components[\"schemas\"][\"StreamMessagesConnectReq\"];\n };\n };\n responses: {\n 200: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"StreamMessagesConnectResp\"];\n };\n };\n /** @description Platform operation failed. */\n 400: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 401: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 403: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 404: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 409: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 422: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 429: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 500: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 503: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n /** @description Platform operation failed. */\n 504: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"NotificationError\"];\n };\n };\n };\n };\n PlatformV2CapabilityDiscoverV2: {\n parameters: {\n query?: never;\n header?: never;\n path?: never;\n cookie?: never;\n };\n requestBody?: never;\n responses: {\n 200: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"CapabilityDiscoverV2Resp\"];\n };\n };\n /** @description Platform operation failed. */\n 400: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"CapabilityErrorResp\"];\n };\n };\n /** @description Platform operation failed. */\n 401: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"CapabilityErrorResp\"];\n };\n };\n /** @description Platform operation failed. */\n 403: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"CapabilityErrorResp\"];\n };\n };\n /** @description Platform operation failed. */\n 500: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"CapabilityErrorResp\"];\n };\n };\n /** @description Platform operation failed. */\n 503: {\n headers: {\n [name: string]: unknown;\n };\n content: {\n \"application/json\": components[\"schemas\"][\"CapabilityErrorResp\"];\n };\n };\n };\n };\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/streams/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC","sourcesContent":["export * from './requests.js';\nexport * from './messages.js';\nexport * from './capabilities.js';\n"]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { StreamJSON } from './requests.js';
|
|
2
|
+
import { StreamTransport } from './transport.js';
|
|
3
|
+
export interface StreamMessage<Payload = StreamJSON> {
|
|
4
|
+
version: 'stream-messages/v1';
|
|
5
|
+
messageId: string;
|
|
6
|
+
correlationId?: string;
|
|
7
|
+
payload: Payload;
|
|
8
|
+
}
|
|
9
|
+
export interface StreamMessagePublishOptions {
|
|
10
|
+
messageId?: string;
|
|
11
|
+
correlationId?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface StreamMessageSubscription {
|
|
14
|
+
readonly disposed: boolean;
|
|
15
|
+
dispose(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export type StreamMessageErrorHandler = (error: StreamMessageError) => void;
|
|
18
|
+
export interface StreamMessageHandle<Payload = StreamJSON> {
|
|
19
|
+
/** Resolves after broker flush, not after receiver acknowledgement. No retry or offline queue. */
|
|
20
|
+
publish(payload: Payload, options?: StreamMessagePublishOptions): Promise<StreamMessage<Payload>>;
|
|
21
|
+
subscribe(handler: (message: StreamMessage<Payload>) => void | Promise<void>, options?: {
|
|
22
|
+
onError?: StreamMessageErrorHandler;
|
|
23
|
+
}): Promise<StreamMessageSubscription>;
|
|
24
|
+
}
|
|
25
|
+
export declare class StreamMessageError extends Error {
|
|
26
|
+
readonly errorCode: string;
|
|
27
|
+
readonly retryable = false;
|
|
28
|
+
constructor(errorCode: string, message: string);
|
|
29
|
+
}
|
|
30
|
+
export declare class StreamMessagesClient {
|
|
31
|
+
private readonly transport;
|
|
32
|
+
private sessions;
|
|
33
|
+
private listeners;
|
|
34
|
+
private disposed;
|
|
35
|
+
private disposing?;
|
|
36
|
+
constructor(transport?: StreamTransport);
|
|
37
|
+
onError(handler: StreamMessageErrorHandler): () => void;
|
|
38
|
+
use<Payload = StreamJSON>(selector: {
|
|
39
|
+
topic: string;
|
|
40
|
+
}): StreamMessageHandle<Payload>;
|
|
41
|
+
dispose(): Promise<void>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { getCurrentAppId } from '../runtime-context.js';
|
|
2
|
+
import { onAppRuntimeDispose } from '../runtime.js';
|
|
3
|
+
import { StreamTransport } from './transport.js';
|
|
4
|
+
export class StreamMessageError extends Error {
|
|
5
|
+
errorCode;
|
|
6
|
+
retryable = false;
|
|
7
|
+
constructor(errorCode, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.errorCode = errorCode;
|
|
10
|
+
this.name = 'StreamMessageError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const encoder = new TextEncoder();
|
|
14
|
+
const uuid = /^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
15
|
+
const MAX_PAYLOAD = 60 * 1024;
|
|
16
|
+
const MAX_ENVELOPE = 64 * 1024;
|
|
17
|
+
const MAX_FLUSH_TIMEOUT_MS = 10_000;
|
|
18
|
+
function plain(value) {
|
|
19
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) &&
|
|
20
|
+
[Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
21
|
+
}
|
|
22
|
+
function json(value, seen = new Set()) {
|
|
23
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
24
|
+
return true;
|
|
25
|
+
if (typeof value === 'number')
|
|
26
|
+
return Number.isFinite(value);
|
|
27
|
+
if ((!plain(value) && !Array.isArray(value)) || seen.has(value))
|
|
28
|
+
return false;
|
|
29
|
+
seen.add(value);
|
|
30
|
+
const valid = (Array.isArray(value) ? Array.from(value) : Object.values(value)).every(v => json(v, seen));
|
|
31
|
+
seen.delete(value);
|
|
32
|
+
return valid;
|
|
33
|
+
}
|
|
34
|
+
function fail(code, message) {
|
|
35
|
+
return new StreamMessageError(`STREAM_MESSAGE_${code}`, message);
|
|
36
|
+
}
|
|
37
|
+
function envelope(value, maxPayload) {
|
|
38
|
+
if (!plain(value) || Object.keys(value).some(k => !['version', 'messageId', 'correlationId', 'payload'].includes(k)) ||
|
|
39
|
+
value.version !== 'stream-messages/v1' || typeof value.messageId !== 'string' || !uuid.test(value.messageId) ||
|
|
40
|
+
(value.correlationId !== undefined && (typeof value.correlationId !== 'string' ||
|
|
41
|
+
encoder.encode(value.correlationId).length > 128 || /[\u0000\r\n]/.test(value.correlationId))) ||
|
|
42
|
+
!json(value.payload))
|
|
43
|
+
return false;
|
|
44
|
+
return encoder.encode(JSON.stringify(value.payload)).length <= maxPayload;
|
|
45
|
+
}
|
|
46
|
+
/** One permission-scoped session. Every connection gets a fresh key and JWT. */
|
|
47
|
+
class MessageSession {
|
|
48
|
+
transport;
|
|
49
|
+
topic;
|
|
50
|
+
mode;
|
|
51
|
+
report;
|
|
52
|
+
remove;
|
|
53
|
+
handler;
|
|
54
|
+
disposed = false;
|
|
55
|
+
runtime;
|
|
56
|
+
connection;
|
|
57
|
+
subscription;
|
|
58
|
+
connecting;
|
|
59
|
+
watching;
|
|
60
|
+
abort = new AbortController();
|
|
61
|
+
reconnectTimer;
|
|
62
|
+
expiryTimer;
|
|
63
|
+
started = false;
|
|
64
|
+
connected = false;
|
|
65
|
+
attempts = 0;
|
|
66
|
+
subject = '';
|
|
67
|
+
payloadLimit = MAX_PAYLOAD;
|
|
68
|
+
expiresAt = 0;
|
|
69
|
+
brokerError;
|
|
70
|
+
disposing;
|
|
71
|
+
removeRuntimeListener;
|
|
72
|
+
constructor(transport, topic, mode, report, remove, handler) {
|
|
73
|
+
this.transport = transport;
|
|
74
|
+
this.topic = topic;
|
|
75
|
+
this.mode = mode;
|
|
76
|
+
this.report = report;
|
|
77
|
+
this.remove = remove;
|
|
78
|
+
this.handler = handler;
|
|
79
|
+
this.runtime = transport.runtime();
|
|
80
|
+
if (this.runtime)
|
|
81
|
+
this.removeRuntimeListener = onAppRuntimeDispose(this.runtime, () => { void this.dispose(); });
|
|
82
|
+
}
|
|
83
|
+
async ready() {
|
|
84
|
+
if (this.disposed)
|
|
85
|
+
throw fail('DISPOSED', 'Stream messages session disposed.');
|
|
86
|
+
if (this.connected)
|
|
87
|
+
return;
|
|
88
|
+
if (this.connecting)
|
|
89
|
+
return this.connecting;
|
|
90
|
+
if (this.started)
|
|
91
|
+
throw fail('DISCONNECTED', 'Stream messages disconnected.');
|
|
92
|
+
this.started = true;
|
|
93
|
+
try {
|
|
94
|
+
await this.connect();
|
|
95
|
+
if (this.disposed)
|
|
96
|
+
throw fail('DISPOSED', 'Stream messages session disposed.');
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
await this.dispose();
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
connect() {
|
|
104
|
+
const operation = this.open();
|
|
105
|
+
this.connecting = operation;
|
|
106
|
+
return operation.finally(() => { if (this.connecting === operation)
|
|
107
|
+
this.connecting = undefined; });
|
|
108
|
+
}
|
|
109
|
+
async open() {
|
|
110
|
+
let nc;
|
|
111
|
+
let key;
|
|
112
|
+
let watched = false;
|
|
113
|
+
try {
|
|
114
|
+
// HTTP-only Cloud Apps must not load broker libraries or require their dependencies.
|
|
115
|
+
const [{ jwtAuthenticator, wsconnect }, { createUser }] = await Promise.all([
|
|
116
|
+
import('@nats-io/nats-core'), import('@nats-io/nkeys'),
|
|
117
|
+
]);
|
|
118
|
+
if (this.disposed)
|
|
119
|
+
return;
|
|
120
|
+
const connectionKey = createUser();
|
|
121
|
+
key = connectionKey;
|
|
122
|
+
const appId = this.transport.runtime()?.appId ?? getCurrentAppId();
|
|
123
|
+
const request = {
|
|
124
|
+
appId, topics: [{ topic: this.topic, publish: this.mode === 'publish', subscribe: this.mode === 'subscribe' }],
|
|
125
|
+
clientPublicKey: connectionKey.getPublicKey(),
|
|
126
|
+
};
|
|
127
|
+
const { value, ok } = await this.transport.post('/openapi/v2/stream-messages/connect', request, this.abort.signal);
|
|
128
|
+
const ticket = this.ticket(value, ok);
|
|
129
|
+
if (this.disposed)
|
|
130
|
+
return;
|
|
131
|
+
const expiresAt = Date.parse(ticket.expiresAt);
|
|
132
|
+
const permission = ticket.permissions[0];
|
|
133
|
+
this.subject = (this.mode === 'publish' ? permission.publishSubject : permission.subscribeSubject);
|
|
134
|
+
this.payloadLimit = ticket.maxPayloadBytes;
|
|
135
|
+
// Disable library reconnection so it cannot buffer writes or reuse expired JWTs.
|
|
136
|
+
nc = await wsconnect({
|
|
137
|
+
servers: [ticket.url], reconnect: false, waitOnFirstConnect: false,
|
|
138
|
+
ignoreClusterUpdates: true, timeout: Math.max(1, Math.min(MAX_FLUSH_TIMEOUT_MS, expiresAt - Date.now())),
|
|
139
|
+
authenticator: jwtAuthenticator(ticket.userJwt, () => connectionKey.getSeed()),
|
|
140
|
+
});
|
|
141
|
+
if (this.disposed) {
|
|
142
|
+
await nc.close();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
this.connection = nc;
|
|
146
|
+
this.expiresAt = expiresAt;
|
|
147
|
+
this.brokerError = undefined;
|
|
148
|
+
watched = true;
|
|
149
|
+
this.watching = this.watch(nc, connectionKey);
|
|
150
|
+
// Renewal also bounds the initial handshake; it must not wait for its first PONG.
|
|
151
|
+
const untilExpiry = expiresAt - Date.now();
|
|
152
|
+
this.expiryTimer = setTimeout(() => {
|
|
153
|
+
if (this.connection !== nc)
|
|
154
|
+
return;
|
|
155
|
+
this.connected = false;
|
|
156
|
+
void nc.close();
|
|
157
|
+
}, Math.max(1, Math.min(2_147_483_647, untilExpiry - Math.min(5_000, untilExpiry / 10))));
|
|
158
|
+
if (this.mode === 'subscribe') {
|
|
159
|
+
this.subscription = nc.subscribe(this.subject, { callback: (error, msg) => {
|
|
160
|
+
if (this.disposed || this.connection !== nc || Date.now() >= expiresAt)
|
|
161
|
+
return;
|
|
162
|
+
if (error) {
|
|
163
|
+
this.report(fail('SUBSCRIBE_FAILED', 'Stream subscription failed.'));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
if (msg.data.length > MAX_ENVELOPE)
|
|
168
|
+
throw fail('INVALID_ENVELOPE', 'Stream message envelope exceeds limit.');
|
|
169
|
+
const value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(msg.data));
|
|
170
|
+
if (!envelope(value, this.payloadLimit))
|
|
171
|
+
throw fail('INVALID_ENVELOPE', 'Invalid Stream message envelope.');
|
|
172
|
+
// Identity comes from the authorized physical subject; never accept identity fields in the envelope.
|
|
173
|
+
const prefix = this.subject.slice(0, -1);
|
|
174
|
+
if (!msg.subject.startsWith(prefix) || !/^[^.\s*>]+$/.test(msg.subject.slice(prefix.length))) {
|
|
175
|
+
throw fail('INVALID_SUBJECT', 'Invalid Stream message subject.');
|
|
176
|
+
}
|
|
177
|
+
let result;
|
|
178
|
+
try {
|
|
179
|
+
result = this.handler(value);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
this.report(fail('HANDLER_FAILED', 'Stream message handler failed.'));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
Promise.resolve(result).catch(() => {
|
|
186
|
+
if (!this.disposed)
|
|
187
|
+
this.report(fail('HANDLER_FAILED', 'Stream message handler failed.'));
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
this.report(error instanceof StreamMessageError ? error : fail('INVALID_ENVELOPE', 'Invalid Stream message envelope.'));
|
|
192
|
+
}
|
|
193
|
+
} });
|
|
194
|
+
}
|
|
195
|
+
await this.flush(nc, false);
|
|
196
|
+
if (this.disposed)
|
|
197
|
+
return;
|
|
198
|
+
if (nc.isClosed() || this.connection !== nc)
|
|
199
|
+
throw fail('CONNECT_FAILED', 'Stream messages connection failed.');
|
|
200
|
+
this.connected = true;
|
|
201
|
+
this.attempts = 0;
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (nc)
|
|
205
|
+
await nc.close();
|
|
206
|
+
if (this.disposed)
|
|
207
|
+
return;
|
|
208
|
+
throw error instanceof StreamMessageError ? error : fail('CONNECT_FAILED', 'Stream messages connection failed.');
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
// Before a successful handoff, this attempt owns its key, including when
|
|
212
|
+
// the issuer or dial fails. A live connection's watcher owns later wiping.
|
|
213
|
+
if (!watched) {
|
|
214
|
+
await nc?.close();
|
|
215
|
+
key?.clear();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async flush(nc, publishing) {
|
|
220
|
+
const failure = () => publishing
|
|
221
|
+
? fail('RESULT_UNKNOWN', 'Stream publish result unknown; do not automatically retry.')
|
|
222
|
+
: fail('CONNECT_FAILED', 'Stream messages connection failed.');
|
|
223
|
+
let timer;
|
|
224
|
+
let stopped;
|
|
225
|
+
try {
|
|
226
|
+
if (this.disposed || nc.isClosed() || this.expiresAt <= Date.now())
|
|
227
|
+
throw failure();
|
|
228
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
229
|
+
stopped = () => reject(failure());
|
|
230
|
+
this.abort.signal.addEventListener('abort', stopped, { once: true });
|
|
231
|
+
if (this.abort.signal.aborted)
|
|
232
|
+
stopped();
|
|
233
|
+
timer = setTimeout(stopped, Math.max(1, Math.min(MAX_FLUSH_TIMEOUT_MS, this.expiresAt - Date.now())));
|
|
234
|
+
});
|
|
235
|
+
await Promise.race([nc.flush(), deadline, nc.closed().then(() => { throw failure(); })]);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
const watching = this.connection === nc ? this.watching : undefined;
|
|
239
|
+
if (this.connection === nc)
|
|
240
|
+
this.connected = false;
|
|
241
|
+
// Stop the transport and wipe its key before reporting a failed/unknown operation.
|
|
242
|
+
await nc.close();
|
|
243
|
+
await watching;
|
|
244
|
+
throw failure();
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
clearTimeout(timer);
|
|
248
|
+
if (stopped)
|
|
249
|
+
this.abort.signal.removeEventListener('abort', stopped);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
ticket(value, ok) {
|
|
253
|
+
if (!ok)
|
|
254
|
+
throw fail('CONNECT_DENIED', 'Stream messages connection denied.');
|
|
255
|
+
if (!plain(value) || value.contractVersion !== 'stream-messages/v1' ||
|
|
256
|
+
typeof value.url !== 'string' || typeof value.userJwt !== 'string' || !value.userJwt ||
|
|
257
|
+
typeof value.expiresAt !== 'string' || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.test(value.expiresAt) ||
|
|
258
|
+
!(Date.parse(value.expiresAt) > Date.now()) ||
|
|
259
|
+
!Number.isSafeInteger(value.maxPayloadBytes) || value.maxPayloadBytes < 1 || value.maxPayloadBytes > MAX_PAYLOAD ||
|
|
260
|
+
!Array.isArray(value.permissions) || value.permissions.length !== 1) {
|
|
261
|
+
throw fail('CONNECT_PROTOCOL_ERROR', 'Invalid Stream messages connection response.');
|
|
262
|
+
}
|
|
263
|
+
const url = new URL(value.url);
|
|
264
|
+
if (url.protocol !== 'wss:' || url.username || url.password || url.hash || url.search) {
|
|
265
|
+
throw fail('CONNECT_PROTOCOL_ERROR', 'Stream messages requires a credential-free WSS URL.');
|
|
266
|
+
}
|
|
267
|
+
const permission = value.permissions[0];
|
|
268
|
+
if (!plain(permission) || permission.topic !== this.topic)
|
|
269
|
+
throw fail('CONNECT_PROTOCOL_ERROR', 'Invalid topic permission.');
|
|
270
|
+
// Subject layout belongs to SYS. Only check NATS syntax and requested access.
|
|
271
|
+
const validSubject = (subject, mode) => {
|
|
272
|
+
if (typeof subject !== 'string' || !subject)
|
|
273
|
+
return false;
|
|
274
|
+
if (mode === 'subscribe' && !subject.endsWith('.*'))
|
|
275
|
+
return false;
|
|
276
|
+
const parts = subject.split('.');
|
|
277
|
+
return parts.every((part, index) => !!part &&
|
|
278
|
+
(mode === 'subscribe' && index === parts.length - 1 && part === '*' || !/[\s*>]/.test(part)));
|
|
279
|
+
};
|
|
280
|
+
const publish = permission.publishSubject;
|
|
281
|
+
const subscribe = permission.subscribeSubject;
|
|
282
|
+
if ((publish !== undefined && !validSubject(publish, 'publish')) ||
|
|
283
|
+
(subscribe !== undefined && !validSubject(subscribe, 'subscribe')) ||
|
|
284
|
+
(this.mode === 'publish' ? !publish || !!subscribe : !subscribe || !!publish)) {
|
|
285
|
+
throw fail('CONNECT_PROTOCOL_ERROR', 'Invalid scoped topic subjects.');
|
|
286
|
+
}
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
async watch(nc, key) {
|
|
290
|
+
try {
|
|
291
|
+
for await (const status of nc.status()) {
|
|
292
|
+
if (this.disposed || this.connection !== nc)
|
|
293
|
+
break;
|
|
294
|
+
if (status.type === 'disconnect' || status.type === 'close' || status.type === 'staleConnection')
|
|
295
|
+
this.connected = false;
|
|
296
|
+
if (status.type === 'error' || status.type === 'slowConsumer') {
|
|
297
|
+
this.brokerError = fail(status.type === 'error' ? 'BROKER_ERROR' : 'SLOW_CONSUMER', 'Stream messages asynchronous broker error.');
|
|
298
|
+
this.report(this.brokerError);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const error = await nc.closed();
|
|
302
|
+
if (error && !this.disposed)
|
|
303
|
+
this.report(fail('CONNECTION_LOST', 'Stream messages connection lost.'));
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
if (!this.disposed)
|
|
307
|
+
this.report(fail('CONNECTION_LOST', 'Stream messages connection lost.'));
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
// Close even if the status iterator failed. A replacement must never be
|
|
311
|
+
// issued while the prior, potentially wider, authenticated socket is live.
|
|
312
|
+
await nc.close();
|
|
313
|
+
key.clear();
|
|
314
|
+
if (this.connection === nc) {
|
|
315
|
+
this.connection = undefined;
|
|
316
|
+
this.connected = false;
|
|
317
|
+
this.subscription = undefined;
|
|
318
|
+
clearTimeout(this.expiryTimer);
|
|
319
|
+
this.scheduleReconnect();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
scheduleReconnect() {
|
|
324
|
+
if (this.disposed || this.reconnectTimer)
|
|
325
|
+
return;
|
|
326
|
+
this.reconnectTimer = setTimeout(() => {
|
|
327
|
+
this.reconnectTimer = undefined;
|
|
328
|
+
if (this.disposed)
|
|
329
|
+
return;
|
|
330
|
+
void this.connect().catch(error => {
|
|
331
|
+
this.report(error instanceof StreamMessageError ? error : fail('CONNECT_FAILED', 'Stream messages reconnection failed.'));
|
|
332
|
+
this.scheduleReconnect();
|
|
333
|
+
});
|
|
334
|
+
}, Math.min(30_000, 500 * 2 ** Math.min(this.attempts++, 6)));
|
|
335
|
+
}
|
|
336
|
+
async publish(value) {
|
|
337
|
+
// Only the very first publish may establish a connection. Subsequent disconnected writes fail.
|
|
338
|
+
if (!this.started)
|
|
339
|
+
await this.ready();
|
|
340
|
+
const nc = this.connection;
|
|
341
|
+
if (this.disposed || !this.connected || !nc || nc.isClosed())
|
|
342
|
+
throw fail('DISCONNECTED', 'Stream messages disconnected.');
|
|
343
|
+
if (!envelope(value, this.payloadLimit))
|
|
344
|
+
throw fail('INVALID_ENVELOPE', 'Invalid Stream message envelope or payload limit exceeded.');
|
|
345
|
+
const bytes = encoder.encode(JSON.stringify(value));
|
|
346
|
+
if (bytes.length > MAX_ENVELOPE)
|
|
347
|
+
throw fail('PAYLOAD_TOO_LARGE', 'Stream message envelope exceeds limit.');
|
|
348
|
+
this.brokerError = undefined;
|
|
349
|
+
try {
|
|
350
|
+
nc.publish(this.subject, bytes);
|
|
351
|
+
await this.flush(nc, true);
|
|
352
|
+
if (this.brokerError)
|
|
353
|
+
throw this.brokerError;
|
|
354
|
+
if (!this.connected || this.connection !== nc)
|
|
355
|
+
throw fail('RESULT_UNKNOWN', 'Stream publish result unknown.');
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
throw error instanceof StreamMessageError ? error : fail('RESULT_UNKNOWN', 'Stream publish result unknown; do not automatically retry.');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
dispose() {
|
|
362
|
+
if (this.disposing)
|
|
363
|
+
return this.disposing;
|
|
364
|
+
this.disposed = true;
|
|
365
|
+
this.connected = false;
|
|
366
|
+
this.abort.abort();
|
|
367
|
+
this.removeRuntimeListener?.();
|
|
368
|
+
clearTimeout(this.reconnectTimer);
|
|
369
|
+
clearTimeout(this.expiryTimer);
|
|
370
|
+
this.subscription?.unsubscribe();
|
|
371
|
+
this.disposing = (async () => {
|
|
372
|
+
await this.connection?.close();
|
|
373
|
+
await this.connecting?.catch(() => undefined);
|
|
374
|
+
await this.watching;
|
|
375
|
+
this.connection = undefined;
|
|
376
|
+
this.remove();
|
|
377
|
+
})();
|
|
378
|
+
return this.disposing;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
export class StreamMessagesClient {
|
|
382
|
+
transport;
|
|
383
|
+
sessions = new Set();
|
|
384
|
+
listeners = new Set();
|
|
385
|
+
disposed = false;
|
|
386
|
+
disposing;
|
|
387
|
+
constructor(transport = new StreamTransport()) {
|
|
388
|
+
this.transport = transport;
|
|
389
|
+
}
|
|
390
|
+
onError(handler) {
|
|
391
|
+
if (this.disposed)
|
|
392
|
+
throw fail('DISPOSED', 'Stream messages client disposed.');
|
|
393
|
+
this.listeners.add(handler);
|
|
394
|
+
return () => { this.listeners.delete(handler); };
|
|
395
|
+
}
|
|
396
|
+
use(selector) {
|
|
397
|
+
if (!plain(selector) || Object.keys(selector).some(k => k !== 'topic') ||
|
|
398
|
+
typeof selector.topic !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,127}$/.test(selector.topic)) {
|
|
399
|
+
throw new TypeError('Tier0 SDK: invalid logical Stream topic.');
|
|
400
|
+
}
|
|
401
|
+
const topic = selector.topic;
|
|
402
|
+
let publisher;
|
|
403
|
+
const session = (mode, handler, onError) => {
|
|
404
|
+
if (this.disposed)
|
|
405
|
+
throw fail('DISPOSED', 'Stream messages client disposed.');
|
|
406
|
+
const report = (error) => {
|
|
407
|
+
const listeners = new Set(this.listeners);
|
|
408
|
+
if (onError)
|
|
409
|
+
listeners.add(onError);
|
|
410
|
+
if (!listeners.size)
|
|
411
|
+
console.error(error);
|
|
412
|
+
for (const listener of listeners) {
|
|
413
|
+
try {
|
|
414
|
+
listener(error);
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
console.error(fail('ERROR_HANDLER_FAILED', 'Stream messages error handler failed.'));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
const value = new MessageSession(this.transport, topic, mode, report, () => this.sessions.delete(value), handler);
|
|
422
|
+
this.sessions.add(value);
|
|
423
|
+
return value;
|
|
424
|
+
};
|
|
425
|
+
return {
|
|
426
|
+
publish: async (payload, options = {}) => {
|
|
427
|
+
if (this.disposed)
|
|
428
|
+
throw fail('DISPOSED', 'Stream messages client disposed.');
|
|
429
|
+
if (!plain(options) || Object.keys(options).some(k => !['messageId', 'correlationId'].includes(k)) ||
|
|
430
|
+
(options.messageId !== undefined && typeof options.messageId !== 'string') ||
|
|
431
|
+
(options.correlationId !== undefined && typeof options.correlationId !== 'string')) {
|
|
432
|
+
throw new TypeError('Tier0 SDK: unsupported Stream publish option.');
|
|
433
|
+
}
|
|
434
|
+
const message = {
|
|
435
|
+
version: 'stream-messages/v1', messageId: options.messageId ?? crypto.randomUUID(),
|
|
436
|
+
...(options.correlationId === undefined ? {} : { correlationId: options.correlationId }), payload,
|
|
437
|
+
};
|
|
438
|
+
if (!envelope(message, MAX_PAYLOAD))
|
|
439
|
+
throw fail('INVALID_ENVELOPE', 'Invalid Stream message envelope.');
|
|
440
|
+
if (encoder.encode(JSON.stringify(message)).length > MAX_ENVELOPE)
|
|
441
|
+
throw fail('PAYLOAD_TOO_LARGE', 'Stream message envelope exceeds limit.');
|
|
442
|
+
if (publisher?.disposed) {
|
|
443
|
+
const previous = publisher;
|
|
444
|
+
await previous.dispose();
|
|
445
|
+
if (previous.runtime !== this.transport.runtime())
|
|
446
|
+
throw fail('DISCONNECTED', 'Stream messages disconnected.');
|
|
447
|
+
if (publisher === previous)
|
|
448
|
+
publisher = undefined;
|
|
449
|
+
}
|
|
450
|
+
if (!publisher)
|
|
451
|
+
publisher = session('publish');
|
|
452
|
+
await publisher.publish(message);
|
|
453
|
+
return message;
|
|
454
|
+
},
|
|
455
|
+
subscribe: async (handler, options = {}) => {
|
|
456
|
+
if (typeof handler !== 'function' || !plain(options) || Object.keys(options).some(k => k !== 'onError') ||
|
|
457
|
+
(options.onError !== undefined && typeof options.onError !== 'function'))
|
|
458
|
+
throw new TypeError('Tier0 SDK: invalid Stream subscription.');
|
|
459
|
+
const value = session('subscribe', handler, options.onError);
|
|
460
|
+
await value.ready();
|
|
461
|
+
return value;
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
dispose() {
|
|
466
|
+
if (this.disposing)
|
|
467
|
+
return this.disposing;
|
|
468
|
+
this.disposed = true;
|
|
469
|
+
this.disposing = Promise.all([...this.sessions].map(session => session.dispose())).then(() => {
|
|
470
|
+
this.listeners.clear();
|
|
471
|
+
});
|
|
472
|
+
return this.disposing;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
//# sourceMappingURL=messages.js.map
|