@gloocan/cat-inspector 0.1.1

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.
Files changed (201) hide show
  1. package/dist/artifact-helpers.d.ts +4 -0
  2. package/dist/artifact-helpers.d.ts.map +1 -0
  3. package/dist/artifact-helpers.js +21 -0
  4. package/dist/artifact-helpers.js.map +1 -0
  5. package/dist/ast/get-all-ts-files.d.ts +5 -0
  6. package/dist/ast/get-all-ts-files.d.ts.map +1 -0
  7. package/dist/ast/get-all-ts-files.js +36 -0
  8. package/dist/ast/get-all-ts-files.js.map +1 -0
  9. package/dist/ast/merge-ast.d.ts +3 -0
  10. package/dist/ast/merge-ast.d.ts.map +1 -0
  11. package/dist/ast/merge-ast.js +40 -0
  12. package/dist/ast/merge-ast.js.map +1 -0
  13. package/dist/ast/run-ast-scanner.d.ts +15 -0
  14. package/dist/ast/run-ast-scanner.d.ts.map +1 -0
  15. package/dist/ast/run-ast-scanner.js +44 -0
  16. package/dist/ast/run-ast-scanner.js.map +1 -0
  17. package/dist/ast/scan-for-labels.d.ts +20 -0
  18. package/dist/ast/scan-for-labels.d.ts.map +1 -0
  19. package/dist/ast/scan-for-labels.js +88 -0
  20. package/dist/ast/scan-for-labels.js.map +1 -0
  21. package/dist/ast/scan-for-returns.d.ts +13 -0
  22. package/dist/ast/scan-for-returns.d.ts.map +1 -0
  23. package/dist/ast/scan-for-returns.js +48 -0
  24. package/dist/ast/scan-for-returns.js.map +1 -0
  25. package/dist/ast/type-expand.d.ts +16 -0
  26. package/dist/ast/type-expand.d.ts.map +1 -0
  27. package/dist/ast/type-expand.js +118 -0
  28. package/dist/ast/type-expand.js.map +1 -0
  29. package/dist/ast/visit-node.d.ts +31 -0
  30. package/dist/ast/visit-node.d.ts.map +1 -0
  31. package/dist/ast/visit-node.js +298 -0
  32. package/dist/ast/visit-node.js.map +1 -0
  33. package/dist/bootstrap.d.ts +105 -0
  34. package/dist/bootstrap.d.ts.map +1 -0
  35. package/dist/bootstrap.js +121 -0
  36. package/dist/bootstrap.js.map +1 -0
  37. package/dist/catalog-bootstrap-cache.d.ts +23 -0
  38. package/dist/catalog-bootstrap-cache.d.ts.map +1 -0
  39. package/dist/catalog-bootstrap-cache.js +54 -0
  40. package/dist/catalog-bootstrap-cache.js.map +1 -0
  41. package/dist/catalog-fingerprint.d.ts +20 -0
  42. package/dist/catalog-fingerprint.d.ts.map +1 -0
  43. package/dist/catalog-fingerprint.js +57 -0
  44. package/dist/catalog-fingerprint.js.map +1 -0
  45. package/dist/coverage/compute-coverage.d.ts +41 -0
  46. package/dist/coverage/compute-coverage.d.ts.map +1 -0
  47. package/dist/coverage/compute-coverage.js +229 -0
  48. package/dist/coverage/compute-coverage.js.map +1 -0
  49. package/dist/coverage/scan-all-service-candidates.d.ts +15 -0
  50. package/dist/coverage/scan-all-service-candidates.d.ts.map +1 -0
  51. package/dist/coverage/scan-all-service-candidates.js +73 -0
  52. package/dist/coverage/scan-all-service-candidates.js.map +1 -0
  53. package/dist/coverage/scan-express-candidates.d.ts +17 -0
  54. package/dist/coverage/scan-express-candidates.d.ts.map +1 -0
  55. package/dist/coverage/scan-express-candidates.js +300 -0
  56. package/dist/coverage/scan-express-candidates.js.map +1 -0
  57. package/dist/coverage/scan-reachable-services.d.ts +13 -0
  58. package/dist/coverage/scan-reachable-services.d.ts.map +1 -0
  59. package/dist/coverage/scan-reachable-services.js +469 -0
  60. package/dist/coverage/scan-reachable-services.js.map +1 -0
  61. package/dist/coverage/types.d.ts +32 -0
  62. package/dist/coverage/types.d.ts.map +1 -0
  63. package/dist/coverage/types.js +2 -0
  64. package/dist/coverage/types.js.map +1 -0
  65. package/dist/decorators/cat-class.d.ts +11 -0
  66. package/dist/decorators/cat-class.d.ts.map +1 -0
  67. package/dist/decorators/cat-class.js +20 -0
  68. package/dist/decorators/cat-class.js.map +1 -0
  69. package/dist/decorators/cat.d.ts +3 -0
  70. package/dist/decorators/cat.d.ts.map +1 -0
  71. package/dist/decorators/cat.js +102 -0
  72. package/dist/decorators/cat.js.map +1 -0
  73. package/dist/express-inspector-correlation.d.ts +23 -0
  74. package/dist/express-inspector-correlation.d.ts.map +1 -0
  75. package/dist/express-inspector-correlation.js +50 -0
  76. package/dist/express-inspector-correlation.js.map +1 -0
  77. package/dist/express-playground-mocks.d.ts +28 -0
  78. package/dist/express-playground-mocks.d.ts.map +1 -0
  79. package/dist/express-playground-mocks.js +55 -0
  80. package/dist/express-playground-mocks.js.map +1 -0
  81. package/dist/express-qa-host-minio-upload.d.ts +16 -0
  82. package/dist/express-qa-host-minio-upload.d.ts.map +1 -0
  83. package/dist/express-qa-host-minio-upload.js +56 -0
  84. package/dist/express-qa-host-minio-upload.js.map +1 -0
  85. package/dist/express.d.ts +21 -0
  86. package/dist/express.d.ts.map +1 -0
  87. package/dist/express.js +167 -0
  88. package/dist/express.js.map +1 -0
  89. package/dist/functional.d.ts +15 -0
  90. package/dist/functional.d.ts.map +1 -0
  91. package/dist/functional.js +165 -0
  92. package/dist/functional.js.map +1 -0
  93. package/dist/graph/relationships.d.ts +9 -0
  94. package/dist/graph/relationships.d.ts.map +1 -0
  95. package/dist/graph/relationships.js +92 -0
  96. package/dist/graph/relationships.js.map +1 -0
  97. package/dist/http-bridge-registry.d.ts +26 -0
  98. package/dist/http-bridge-registry.d.ts.map +1 -0
  99. package/dist/http-bridge-registry.js +39 -0
  100. package/dist/http-bridge-registry.js.map +1 -0
  101. package/dist/index.d.ts +36 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +35 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/invoke-policy.d.ts +34 -0
  106. package/dist/invoke-policy.d.ts.map +1 -0
  107. package/dist/invoke-policy.js +72 -0
  108. package/dist/invoke-policy.js.map +1 -0
  109. package/dist/invoke-runtime-config.d.ts +4 -0
  110. package/dist/invoke-runtime-config.d.ts.map +1 -0
  111. package/dist/invoke-runtime-config.js +15 -0
  112. package/dist/invoke-runtime-config.js.map +1 -0
  113. package/dist/jobs/in-memory-job-registry.d.ts +21 -0
  114. package/dist/jobs/in-memory-job-registry.d.ts.map +1 -0
  115. package/dist/jobs/in-memory-job-registry.js +44 -0
  116. package/dist/jobs/in-memory-job-registry.js.map +1 -0
  117. package/dist/logger.d.ts +9 -0
  118. package/dist/logger.d.ts.map +1 -0
  119. package/dist/logger.js +17 -0
  120. package/dist/logger.js.map +1 -0
  121. package/dist/openapi/registry-to-openapi.d.ts +11 -0
  122. package/dist/openapi/registry-to-openapi.d.ts.map +1 -0
  123. package/dist/openapi/registry-to-openapi.js +61 -0
  124. package/dist/openapi/registry-to-openapi.js.map +1 -0
  125. package/dist/registry-state.d.ts +107 -0
  126. package/dist/registry-state.d.ts.map +1 -0
  127. package/dist/registry-state.js +302 -0
  128. package/dist/registry-state.js.map +1 -0
  129. package/dist/return.d.ts +14 -0
  130. package/dist/return.d.ts.map +1 -0
  131. package/dist/return.js +107 -0
  132. package/dist/return.js.map +1 -0
  133. package/dist/rpc.d.ts +7 -0
  134. package/dist/rpc.d.ts.map +1 -0
  135. package/dist/rpc.js +287 -0
  136. package/dist/rpc.js.map +1 -0
  137. package/dist/serialize-rpc-result.d.ts +38 -0
  138. package/dist/serialize-rpc-result.d.ts.map +1 -0
  139. package/dist/serialize-rpc-result.js +132 -0
  140. package/dist/serialize-rpc-result.js.map +1 -0
  141. package/dist/session-store.d.ts +9 -0
  142. package/dist/session-store.d.ts.map +1 -0
  143. package/dist/session-store.js +19 -0
  144. package/dist/session-store.js.map +1 -0
  145. package/dist/source-utils.d.ts +19 -0
  146. package/dist/source-utils.d.ts.map +1 -0
  147. package/dist/source-utils.js +65 -0
  148. package/dist/source-utils.js.map +1 -0
  149. package/dist/transport/remote-bridge-cli.d.ts +2 -0
  150. package/dist/transport/remote-bridge-cli.d.ts.map +1 -0
  151. package/dist/transport/remote-bridge-cli.js +12 -0
  152. package/dist/transport/remote-bridge-cli.js.map +1 -0
  153. package/dist/transport/remote-inspector-bridge.d.ts +18 -0
  154. package/dist/transport/remote-inspector-bridge.d.ts.map +1 -0
  155. package/dist/transport/remote-inspector-bridge.js +71 -0
  156. package/dist/transport/remote-inspector-bridge.js.map +1 -0
  157. package/dist/transport/socket-io-playground.d.ts +78 -0
  158. package/dist/transport/socket-io-playground.d.ts.map +1 -0
  159. package/dist/transport/socket-io-playground.js +875 -0
  160. package/dist/transport/socket-io-playground.js.map +1 -0
  161. package/dist/transport/socket-io.d.ts +4 -0
  162. package/dist/transport/socket-io.d.ts.map +1 -0
  163. package/dist/transport/socket-io.js +4 -0
  164. package/dist/transport/socket-io.js.map +1 -0
  165. package/dist/transport/ws-server.d.ts +42 -0
  166. package/dist/transport/ws-server.d.ts.map +1 -0
  167. package/dist/transport/ws-server.js +429 -0
  168. package/dist/transport/ws-server.js.map +1 -0
  169. package/dist/type-string-normalize.d.ts +24 -0
  170. package/dist/type-string-normalize.d.ts.map +1 -0
  171. package/dist/type-string-normalize.js +178 -0
  172. package/dist/type-string-normalize.js.map +1 -0
  173. package/dist/types.d.ts +218 -0
  174. package/dist/types.d.ts.map +1 -0
  175. package/dist/types.js +2 -0
  176. package/dist/types.js.map +1 -0
  177. package/dist/upload/fetch-file-url.d.ts +24 -0
  178. package/dist/upload/fetch-file-url.d.ts.map +1 -0
  179. package/dist/upload/fetch-file-url.js +130 -0
  180. package/dist/upload/fetch-file-url.js.map +1 -0
  181. package/dist/upload/host-minio-client.d.ts +30 -0
  182. package/dist/upload/host-minio-client.d.ts.map +1 -0
  183. package/dist/upload/host-minio-client.js +56 -0
  184. package/dist/upload/host-minio-client.js.map +1 -0
  185. package/dist/upload/materialize.d.ts +110 -0
  186. package/dist/upload/materialize.d.ts.map +1 -0
  187. package/dist/upload/materialize.js +466 -0
  188. package/dist/upload/materialize.js.map +1 -0
  189. package/dist/upload/upload-store.d.ts +44 -0
  190. package/dist/upload/upload-store.d.ts.map +1 -0
  191. package/dist/upload/upload-store.js +86 -0
  192. package/dist/upload/upload-store.js.map +1 -0
  193. package/dist/validate-params-json-schema.d.ts +13 -0
  194. package/dist/validate-params-json-schema.d.ts.map +1 -0
  195. package/dist/validate-params-json-schema.js +37 -0
  196. package/dist/validate-params-json-schema.js.map +1 -0
  197. package/dist/validate-return-json-schema.d.ts +9 -0
  198. package/dist/validate-return-json-schema.d.ts.map +1 -0
  199. package/dist/validate-return-json-schema.js +33 -0
  200. package/dist/validate-return-json-schema.js.map +1 -0
  201. package/package.json +63 -0
@@ -0,0 +1,875 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { performance } from 'node:perf_hooks';
3
+ import { bootstrap } from '../bootstrap.js';
4
+ import { computeCoverageReport } from '../coverage/compute-coverage.js';
5
+ import { createExpressPlaygroundMocks, } from '../express-playground-mocks.js';
6
+ import { validateBootstrapStorage, } from '../bootstrap.js';
7
+ import { extractArtifactsFromResult } from '../artifact-helpers.js';
8
+ import { setInvokeTimeoutMs } from '../invoke-runtime-config.js';
9
+ import { executeRPC } from '../rpc.js';
10
+ import { configureInvokeRateLimit, invokeAudit, invokeRateLimitAllow, invokeRateLimitRetryAfterMs, registerPreInvoke, runPreInvokes, } from '../invoke-policy.js';
11
+ import { ActiveContext, ApiContext, broadcast, clearBroadcastSink, clearExpressApiInvokeCapture, ErrorCapture, getInspectorBroadcastStore, InstanceRegistry, LabelCapture, Registry, readExpressApiInvokeCapture, runWithInspectorBroadcastTarget, setBroadcastSink, } from '../registry-state.js';
12
+ import { getShape, getType } from '../return.js';
13
+ import { maybeSerializeRpcResult } from '../serialize-rpc-result.js';
14
+ import { sessionCreate, sessionStep } from '../session-store.js';
15
+ import { PROTOCOL_VERSION, } from '../types.js';
16
+ import { putBufferAndPresignGetUrl } from '../upload/host-minio-client.js';
17
+ import { InMemoryUploadStore } from '../upload/upload-store.js';
18
+ import { buildCatalogWireExtras, enrichRegistryParamsWithWireHints, materializeExpressPayloadForInvoke, materializeServiceArgsForInvoke, normalizeExpressPayloadFilesForPlayground, } from '../upload/materialize.js';
19
+ import { computeCatalogFingerprint } from '../catalog-fingerprint.js';
20
+ import { createCatalogBootstrapCache } from '../catalog-bootstrap-cache.js';
21
+ export const INSPECTOR_BROADCAST_EVENT = 'inspector:broadcast';
22
+ export { INSPECTOR_SOCKET_ID_HEADER } from '../registry-state.js';
23
+ function rpcValidationError(requestId, fnKey, label, message, start) {
24
+ return {
25
+ type: 'RPC_RESPONSE',
26
+ requestId,
27
+ fnKey,
28
+ status: 'error',
29
+ result: null,
30
+ returnType: 'error',
31
+ returnShape: null,
32
+ label,
33
+ duration: `${(performance.now() - start).toFixed(2)}ms`,
34
+ error: { message, stack: null, layer: 'validation', code: label },
35
+ };
36
+ }
37
+ function readSocketAuthToken(socket) {
38
+ const a = socket.handshake.auth;
39
+ return typeof a?.token === 'string' ? a.token : undefined;
40
+ }
41
+ function rpcThrownError(requestId, fnKey, label, message, start, layer, stack) {
42
+ return {
43
+ type: 'RPC_RESPONSE',
44
+ requestId,
45
+ fnKey,
46
+ status: 'error',
47
+ result: null,
48
+ returnType: 'error',
49
+ returnShape: null,
50
+ label,
51
+ duration: `${(performance.now() - start).toFixed(2)}ms`,
52
+ error: {
53
+ message,
54
+ stack,
55
+ layer,
56
+ code: layer === 'unexpected' ? 'UNEXPECTED_ERROR' : label,
57
+ },
58
+ };
59
+ }
60
+ export async function invokeExpressPlayground(requestId, fnKey, expressPayload, createMocks) {
61
+ const start = performance.now();
62
+ if (!fnKey.includes('.')) {
63
+ return rpcValidationError(requestId, fnKey, 'INVALID_FN_KEY', 'must be ClassName.methodName', start);
64
+ }
65
+ const entry = Registry.get(fnKey);
66
+ if (!entry) {
67
+ return rpcValidationError(requestId, fnKey, 'FN_NOT_FOUND', 'Not found', start);
68
+ }
69
+ if (entry.mode !== 'api_candidate' && entry.mode !== 'api') {
70
+ return rpcValidationError(requestId, fnKey, 'NOT_EXPRESS', `fnKey is not an Express handler (mode=${entry.mode})`, start);
71
+ }
72
+ const resolved = resolveExpressCallableForInvoke(requestId, fnKey, entry, start);
73
+ if ('error' in resolved)
74
+ return resolved.error;
75
+ const { callable, callThis } = resolved;
76
+ clearExpressApiInvokeCapture();
77
+ normalizeExpressPayloadFilesForPlayground(expressPayload);
78
+ const { req, res, next, getCapture } = createMocks(expressPayload);
79
+ LabelCapture.clear();
80
+ ErrorCapture.clear();
81
+ try {
82
+ if (entry.mode === 'api')
83
+ ApiContext.set(fnKey);
84
+ else
85
+ ActiveContext.push(fnKey);
86
+ const paramCount = entry.params.length;
87
+ let returnValue;
88
+ if (paramCount >= 3) {
89
+ returnValue = await callable.call(callThis, req, res, next);
90
+ }
91
+ else {
92
+ returnValue = await callable.call(callThis, req, res);
93
+ }
94
+ const capture = getCapture();
95
+ const apiCapture = readExpressApiInvokeCapture(fnKey);
96
+ const rawResult = {
97
+ express: {
98
+ ...capture,
99
+ handlerReturn: returnValue,
100
+ },
101
+ };
102
+ const ser = maybeSerializeRpcResult(rawResult);
103
+ if (!ser.ok) {
104
+ const duration = `${(performance.now() - start).toFixed(2)}ms`;
105
+ broadcast({
106
+ event: 'RPC_EXECUTED',
107
+ requestId,
108
+ fnKey,
109
+ label: 'RESULT_NOT_SERIALIZABLE',
110
+ status: 'error',
111
+ duration,
112
+ });
113
+ return rpcValidationError(requestId, fnKey, 'RESULT_NOT_SERIALIZABLE', ser.message, start);
114
+ }
115
+ const result = ser.value;
116
+ const duration = `${(performance.now() - start).toFixed(2)}ms`;
117
+ const rpcLabel = entry.mode === 'api' && apiCapture?.label ? apiCapture.label : LabelCapture.read();
118
+ const bodyForType = entry.mode === 'api' && apiCapture && capture.body !== undefined && capture.body !== null
119
+ ? capture.body
120
+ : undefined;
121
+ const rpcReturnType = entry.mode === 'api' && bodyForType !== undefined ? getType(bodyForType) : getType(returnValue);
122
+ const rpcReturnShape = entry.mode === 'api' && bodyForType !== undefined ? getShape(bodyForType) : getShape(result);
123
+ broadcast({
124
+ event: 'RPC_EXECUTED',
125
+ requestId,
126
+ fnKey,
127
+ label: rpcLabel,
128
+ status: 'ok',
129
+ duration,
130
+ });
131
+ const artifacts = extractArtifactsFromResult(result);
132
+ return {
133
+ type: 'RPC_RESPONSE',
134
+ requestId,
135
+ fnKey,
136
+ status: 'ok',
137
+ result,
138
+ returnType: rpcReturnType,
139
+ returnShape: rpcReturnShape,
140
+ label: rpcLabel,
141
+ duration,
142
+ error: null,
143
+ ...(artifacts ? { artifacts } : {}),
144
+ };
145
+ }
146
+ catch (err) {
147
+ const duration = `${(performance.now() - start).toFixed(2)}ms`;
148
+ const message = err instanceof Error ? err.message : 'error';
149
+ const stack = err instanceof Error ? (err.stack ?? null) : null;
150
+ const captured = ErrorCapture.read();
151
+ const label = captured?.label ?? 'UNEXPECTED_ERROR';
152
+ const layer = captured ? 'expected' : 'unexpected';
153
+ broadcast({
154
+ event: 'RPC_EXECUTED',
155
+ requestId,
156
+ fnKey,
157
+ label,
158
+ status: 'error',
159
+ duration,
160
+ });
161
+ return rpcThrownError(requestId, fnKey, label, message, start, layer, stack);
162
+ }
163
+ finally {
164
+ ApiContext.clear();
165
+ ActiveContext.pop();
166
+ clearExpressApiInvokeCapture();
167
+ }
168
+ }
169
+ export function resolveExpressCallableForInvoke(requestId, fnKey, entry, start) {
170
+ const [className, methodName] = fnKey.split('.');
171
+ if (entry.style === 'function') {
172
+ const callable = typeof entry.originalFn === 'function'
173
+ ? entry.originalFn
174
+ : null;
175
+ if (!callable) {
176
+ return {
177
+ error: rpcValidationError(requestId, fnKey, 'NOT_A_FUNCTION', `${fnKey} not callable`, start),
178
+ };
179
+ }
180
+ return { callable, callThis: undefined };
181
+ }
182
+ const instance = InstanceRegistry.get(className);
183
+ if (!instance) {
184
+ return {
185
+ error: rpcValidationError(requestId, fnKey, 'NO_INSTANCE', `No instance for ${className}`, start),
186
+ };
187
+ }
188
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
+ const maybeFn = instance[methodName];
190
+ if (typeof maybeFn !== 'function') {
191
+ return {
192
+ error: rpcValidationError(requestId, fnKey, 'NOT_A_FUNCTION', `${methodName} not callable`, start),
193
+ };
194
+ }
195
+ return { callable: maybeFn, callThis: instance };
196
+ }
197
+ /**
198
+ * Register Socket.IO catalog + RPC playground handlers and optional broadcast bridge.
199
+ */
200
+ export function attachCatRPC(io, options) {
201
+ validateBootstrapStorage(options.storage ?? options.bootstrap?.storage);
202
+ configureInvokeRateLimit(options.invokeRateLimit ?? null);
203
+ setInvokeTimeoutMs(options.invokeTimeoutMs ?? options.bootstrap?.invokeTimeoutMs);
204
+ const hostMinioResolved = options.bootstrap?.hostMinio;
205
+ const mergedQaFileWireMode = options.qaFileWire?.mode ?? options.bootstrap?.qaFileWire?.mode ?? 'ref';
206
+ const mergedQaMediaUploadTarget = options.qaMediaUpload?.target ?? options.bootstrap?.qaMediaUpload?.target ?? 'admin';
207
+ const qaHostMediaUploadViaSocket = mergedQaFileWireMode === 'url' &&
208
+ mergedQaMediaUploadTarget === 'host' &&
209
+ Boolean(hostMinioResolved);
210
+ const hostMediaUploadStore = qaHostMediaUploadViaSocket
211
+ ? new InMemoryUploadStore({
212
+ maxSizeBytes: options.upload?.maxSizeBytes ?? 50 * 1024 * 1024,
213
+ idleTimeoutMs: options.upload?.idleTimeoutMs ?? 60_000,
214
+ })
215
+ : null;
216
+ const isDevelopment = options.isDevelopment ?? process.env.NODE_ENV !== 'production';
217
+ const rpcAuthSecret = options.rpcAuth?.token;
218
+ let unregisterRpcAuth;
219
+ if (rpcAuthSecret && !isDevelopment) {
220
+ unregisterRpcAuth = registerPreInvoke((ctx) => {
221
+ if (ctx.authToken !== rpcAuthSecret) {
222
+ const rid = ctx.requestId ?? 'unknown';
223
+ return rpcValidationError(rid, ctx.fnKey, 'UNAUTHENTICATED', 'missing or invalid auth.token for RPC', performance.now());
224
+ }
225
+ return undefined;
226
+ });
227
+ }
228
+ const forwardBroadcast = options.forwardBroadcast !== false;
229
+ const emitUnscoped = options.emitUnscopedBroadcasts === true;
230
+ const serverId = options.serverId ?? 'cat-inspector';
231
+ const createMocks = options.createExpressMocks ?? createExpressPlaygroundMocks;
232
+ const hooks = options.hooks ?? {};
233
+ const uploadEnabled = options.upload?.enabled === true;
234
+ const uploadStore = uploadEnabled
235
+ ? new InMemoryUploadStore({
236
+ maxSizeBytes: options.upload?.maxSizeBytes ?? 50 * 1024 * 1024,
237
+ idleTimeoutMs: options.upload?.idleTimeoutMs ?? 60_000,
238
+ })
239
+ : null;
240
+ let httpInspectorBroadcastEnabled = false;
241
+ function computeFingerprint() {
242
+ return computeCatalogFingerprint({
243
+ scanRoots: options.scanRoots,
244
+ getAllTsFilesOptions: options.bootstrap?.getAllTsFilesOptions,
245
+ compilerOptions: options.bootstrap?.compilerOptions,
246
+ expandParamTypes: options.bootstrap?.expandParamTypes,
247
+ expandParamTypesOptions: options.bootstrap?.expandParamTypesOptions,
248
+ redactBodies: options.bootstrap?.redactBodies,
249
+ protocolVersion: Number(PROTOCOL_VERSION),
250
+ });
251
+ }
252
+ async function computeBootstrapPayload(catalogHash) {
253
+ const inner = { ...options.bootstrap };
254
+ if (inner && 'hostMinio' in inner) {
255
+ delete inner.hostMinio;
256
+ }
257
+ const boot = await bootstrap({
258
+ scanRoots: options.scanRoots,
259
+ wsPort: 0,
260
+ enableWebSocket: false,
261
+ registerSignalHandlers: false,
262
+ logLevel: 'error',
263
+ ...inner,
264
+ storage: options.storage ?? options.bootstrap?.storage,
265
+ rpcSerialization: options.rpcSerialization ?? options.bootstrap?.rpcSerialization,
266
+ invokeTimeoutMs: options.invokeTimeoutMs ?? options.bootstrap?.invokeTimeoutMs,
267
+ qaFileWire: options.qaFileWire ?? options.bootstrap?.qaFileWire,
268
+ qaMediaUpload: options.qaMediaUpload ?? options.bootstrap?.qaMediaUpload,
269
+ fileUrl: options.fileUrl ?? options.bootstrap?.fileUrl,
270
+ qaMediaUploadHostUploadUrl: options.qaMediaUploadHostUploadUrl ?? options.bootstrap?.qaMediaUploadHostUploadUrl,
271
+ });
272
+ await boot.shutdown();
273
+ const wireExtras = buildCatalogWireExtras({
274
+ qaFileWire: options.qaFileWire ?? options.bootstrap?.qaFileWire,
275
+ qaMediaUpload: options.qaMediaUpload ?? options.bootstrap?.qaMediaUpload,
276
+ fileUrl: options.fileUrl ?? options.bootstrap?.fileUrl,
277
+ qaMediaUploadHostUploadUrl: options.qaMediaUploadHostUploadUrl ?? options.bootstrap?.qaMediaUploadHostUploadUrl,
278
+ });
279
+ const registryForWire = enrichRegistryParamsWithWireHints(boot.registry, wireExtras);
280
+ return {
281
+ event: 'BOOTSTRAP',
282
+ protocolVersion: Number(PROTOCOL_VERSION),
283
+ catalogHash,
284
+ registry: registryForWire,
285
+ tree: boot.tree,
286
+ qaFileWire: wireExtras.qaFileWire,
287
+ ...(wireExtras.qaMediaUpload ? { qaMediaUpload: wireExtras.qaMediaUpload } : {}),
288
+ ...(wireExtras.fileUrl ? { fileUrl: wireExtras.fileUrl } : {}),
289
+ ...(wireExtras.qaMediaUploadHostUploadUrl
290
+ ? { qaMediaUploadHostUploadUrl: wireExtras.qaMediaUploadHostUploadUrl }
291
+ : {}),
292
+ ...(qaHostMediaUploadViaSocket ? { qaHostMediaUploadViaSocket: true } : {}),
293
+ };
294
+ }
295
+ const catalogCache = createCatalogBootstrapCache({
296
+ computeFingerprint,
297
+ computePayload: computeBootstrapPayload,
298
+ });
299
+ function withSecretBootstrapPayload(payload) {
300
+ const t = options.secretApiKey?.trim();
301
+ return t ? { ...payload, secretApiKey: t } : payload;
302
+ }
303
+ const sinkRegistered = forwardBroadcast;
304
+ if (sinkRegistered) {
305
+ setBroadcastSink((data) => {
306
+ const store = getInspectorBroadcastStore();
307
+ if (!store) {
308
+ if (emitUnscoped) {
309
+ io.emit(INSPECTOR_BROADCAST_EVENT, data);
310
+ }
311
+ return;
312
+ }
313
+ const { socketId, source } = store;
314
+ if (!forwardBroadcast)
315
+ return;
316
+ if (source === 'http') {
317
+ if (!isDevelopment || !httpInspectorBroadcastEnabled)
318
+ return;
319
+ }
320
+ io.to(socketId).emit(INSPECTOR_BROADCAST_EVENT, data);
321
+ });
322
+ }
323
+ function emitHttpInspectorState() {
324
+ io.emit('playground:httpInspector:state', {
325
+ supported: isDevelopment,
326
+ enabled: httpInspectorBroadcastEnabled,
327
+ });
328
+ }
329
+ function onConnection(socket) {
330
+ void (async () => {
331
+ socket.emit('status', {
332
+ connectedAt: new Date().toISOString(),
333
+ server: serverId,
334
+ socketId: socket.id,
335
+ httpInspector: {
336
+ supported: isDevelopment,
337
+ enabled: httpInspectorBroadcastEnabled,
338
+ },
339
+ });
340
+ try {
341
+ await hooks.onConnection?.(socket);
342
+ }
343
+ catch {
344
+ /* optional hook */
345
+ }
346
+ try {
347
+ socket.emit('catalog:bootstrap', withSecretBootstrapPayload(await catalogCache.get()));
348
+ }
349
+ catch (e) {
350
+ const msg = e instanceof Error ? e.message : 'error';
351
+ socket.emit('catalog:error', { message: msg });
352
+ try {
353
+ hooks.onCatalogError?.(socket, e);
354
+ }
355
+ catch {
356
+ /* */
357
+ }
358
+ }
359
+ socket.on('catalog:refresh', async () => {
360
+ try {
361
+ socket.emit('catalog:bootstrap', withSecretBootstrapPayload(await catalogCache.refresh()));
362
+ }
363
+ catch (e) {
364
+ const msg = e instanceof Error ? e.message : 'error';
365
+ socket.emit('catalog:error', { message: msg });
366
+ try {
367
+ hooks.onCatalogError?.(socket, e);
368
+ }
369
+ catch {
370
+ /* */
371
+ }
372
+ }
373
+ });
374
+ socket.on('playground:session:create', (raw, ack) => {
375
+ try {
376
+ const sessionKey = raw && typeof raw === 'object' && 'sessionKey' in raw
377
+ ? String(raw.sessionKey ?? '')
378
+ : undefined;
379
+ const requestId = randomUUID();
380
+ const { sessionId } = sessionCreate(sessionKey || undefined);
381
+ ack?.({
382
+ ok: true,
383
+ requestId,
384
+ sessionId,
385
+ protocolVersion: Number(PROTOCOL_VERSION),
386
+ });
387
+ }
388
+ catch (e) {
389
+ const msg = e instanceof Error ? e.message : 'error';
390
+ ack?.({ ok: false, message: msg });
391
+ }
392
+ });
393
+ socket.on('playground:session:step', (raw, ack) => {
394
+ try {
395
+ if (!raw || typeof raw !== 'object') {
396
+ ack?.({ ok: false, message: 'expected object' });
397
+ return;
398
+ }
399
+ const o = raw;
400
+ const sessionId = typeof o.sessionId === 'string' ? o.sessionId : '';
401
+ const step = typeof o.step === 'string' ? o.step : '';
402
+ if (!sessionId || !step) {
403
+ ack?.({ ok: false, message: 'sessionId and step required' });
404
+ return;
405
+ }
406
+ const { data } = sessionStep(sessionId, step, o.payload);
407
+ ack?.({
408
+ ok: true,
409
+ protocolVersion: Number(PROTOCOL_VERSION),
410
+ sessionId,
411
+ data,
412
+ });
413
+ }
414
+ catch (e) {
415
+ const msg = e instanceof Error ? e.message : 'error';
416
+ ack?.({ ok: false, message: msg });
417
+ }
418
+ });
419
+ socket.on('coverage:request', async () => {
420
+ try {
421
+ const boot = await catalogCache.get();
422
+ const { report } = computeCoverageReport({
423
+ scanRoots: options.scanRoots,
424
+ registrySnapshot: boot.registry,
425
+ });
426
+ socket.emit('coverage:report', {
427
+ ...report,
428
+ meta: { ...report.meta, protocolVersion: Number(PROTOCOL_VERSION) },
429
+ });
430
+ }
431
+ catch (e) {
432
+ const msg = e instanceof Error ? e.message : 'error';
433
+ socket.emit('coverage:error', { message: msg });
434
+ }
435
+ });
436
+ socket.on('playground:setHttpInspector', (raw) => {
437
+ let enabled = false;
438
+ if (raw && typeof raw === 'object' && 'enabled' in raw) {
439
+ enabled = Boolean(raw.enabled);
440
+ }
441
+ if (!isDevelopment) {
442
+ socket.emit('playground:httpInspector:ack', {
443
+ enabled,
444
+ applied: false,
445
+ reason: 'not_development',
446
+ });
447
+ return;
448
+ }
449
+ httpInspectorBroadcastEnabled = enabled;
450
+ socket.emit('playground:httpInspector:ack', {
451
+ enabled: httpInspectorBroadcastEnabled,
452
+ applied: true,
453
+ });
454
+ emitHttpInspectorState();
455
+ });
456
+ socket.on('rpc:call', async (raw) => {
457
+ const start = performance.now();
458
+ const rid = randomUUID();
459
+ await runWithInspectorBroadcastTarget(socket.id, async () => {
460
+ try {
461
+ const early = await hooks.onBeforeRpc?.(socket, raw);
462
+ if (early) {
463
+ socket.emit('rpc:response', early);
464
+ await hooks.onAfterRpc?.(socket, early.requestId, early);
465
+ return;
466
+ }
467
+ if (!raw || typeof raw !== 'object') {
468
+ socket.emit('rpc:response', rpcValidationError(rid, '', 'INVALID_PAYLOAD', 'expected object', start));
469
+ return;
470
+ }
471
+ const p = raw;
472
+ const requestId = typeof p.requestId === 'string' ? p.requestId : '';
473
+ const fnKey = typeof p.fnKey === 'string' ? p.fnKey : '';
474
+ if (!requestId || !fnKey) {
475
+ socket.emit('rpc:response', rpcValidationError(requestId || rid, fnKey, 'INVALID_PAYLOAD', 'requestId and fnKey are required', start));
476
+ return;
477
+ }
478
+ const kind = p.kind;
479
+ let resp;
480
+ if (kind === 'service') {
481
+ if (!Array.isArray(p.args)) {
482
+ resp = rpcValidationError(requestId, fnKey, 'INVALID_PAYLOAD', 'args must be an array', start);
483
+ socket.emit('rpc:response', resp);
484
+ await hooks.onAfterRpc?.(socket, requestId, resp);
485
+ return;
486
+ }
487
+ const entry = Registry.get(fnKey);
488
+ if (entry && entry.mode !== 'service') {
489
+ resp = rpcValidationError(requestId, fnKey, 'KIND_MISMATCH', 'use kind: express for this fnKey', start);
490
+ socket.emit('rpc:response', resp);
491
+ await hooks.onAfterRpc?.(socket, requestId, resp);
492
+ return;
493
+ }
494
+ let nextArgs = p.args;
495
+ const mergedFileUrl = options.fileUrl ?? options.bootstrap?.fileUrl;
496
+ const mergedQaFileWire = options.qaFileWire ?? options.bootstrap?.qaFileWire;
497
+ const canMatRefs = Boolean(uploadEnabled && uploadStore && entry);
498
+ const canMatUrls = Boolean(entry && mergedFileUrl?.allowedHosts && mergedFileUrl.allowedHosts.length > 0);
499
+ if (entry && (canMatRefs || canMatUrls)) {
500
+ try {
501
+ const wantsFile = entry.params.some((pp) => {
502
+ const t = String(pp.type ?? '');
503
+ return /\bFile\b|\bBlob\b/.test(t);
504
+ });
505
+ nextArgs = await materializeServiceArgsForInvoke({
506
+ entry,
507
+ args: nextArgs,
508
+ socketId: socket.id,
509
+ uploadStore: uploadStore ?? null,
510
+ fileUrl: mergedFileUrl ?? null,
511
+ qaFileWire: mergedQaFileWire,
512
+ materializeAs: wantsFile ? 'file' : 'buffer',
513
+ });
514
+ }
515
+ catch (err) {
516
+ const message = err instanceof Error ? err.message : 'error';
517
+ resp = rpcValidationError(requestId, fnKey, 'UPLOAD_MATERIALIZE_FAILED', message, start);
518
+ socket.emit('rpc:response', resp);
519
+ await hooks.onAfterRpc?.(socket, requestId, resp);
520
+ return;
521
+ }
522
+ }
523
+ const tRpc = performance.now();
524
+ const pre = await runPreInvokes({
525
+ fnKey,
526
+ args: nextArgs,
527
+ socketId: socket.id,
528
+ transport: 'socket.io',
529
+ requestId,
530
+ authToken: readSocketAuthToken(socket),
531
+ });
532
+ if (pre) {
533
+ socket.emit('rpc:response', pre);
534
+ await hooks.onAfterRpc?.(socket, pre.requestId, pre);
535
+ await invokeAudit({
536
+ fnKey,
537
+ requestId: pre.requestId,
538
+ status: pre.status,
539
+ transport: 'socket.io',
540
+ socketId: socket.id,
541
+ durationMs: performance.now() - tRpc,
542
+ });
543
+ return;
544
+ }
545
+ if (!invokeRateLimitAllow(socket.id)) {
546
+ resp = rpcValidationError(requestId, fnKey, 'RATE_LIMITED', `retry after ${invokeRateLimitRetryAfterMs(socket.id)}ms`, start);
547
+ socket.emit('rpc:response', resp);
548
+ await hooks.onAfterRpc?.(socket, requestId, resp);
549
+ await invokeAudit({
550
+ fnKey,
551
+ requestId,
552
+ status: 'error',
553
+ transport: 'socket.io',
554
+ socketId: socket.id,
555
+ durationMs: performance.now() - tRpc,
556
+ });
557
+ return;
558
+ }
559
+ resp = await executeRPC({ requestId, fnKey, args: nextArgs });
560
+ socket.emit('rpc:response', resp);
561
+ await hooks.onAfterRpc?.(socket, requestId, resp);
562
+ await invokeAudit({
563
+ fnKey,
564
+ requestId,
565
+ status: resp.status,
566
+ transport: 'socket.io',
567
+ socketId: socket.id,
568
+ durationMs: performance.now() - tRpc,
569
+ });
570
+ return;
571
+ }
572
+ if (kind === 'express') {
573
+ const ex = p.express;
574
+ if (ex !== undefined && ex !== null && typeof ex !== 'object') {
575
+ resp = rpcValidationError(requestId, fnKey, 'INVALID_PAYLOAD', 'express must be an object', start);
576
+ socket.emit('rpc:response', resp);
577
+ await hooks.onAfterRpc?.(socket, requestId, resp);
578
+ return;
579
+ }
580
+ const entry = Registry.get(fnKey);
581
+ if (entry && entry.mode === 'service') {
582
+ resp = rpcValidationError(requestId, fnKey, 'KIND_MISMATCH', 'use kind: service for this fnKey', start);
583
+ socket.emit('rpc:response', resp);
584
+ await hooks.onAfterRpc?.(socket, requestId, resp);
585
+ return;
586
+ }
587
+ let expressPayload = (ex ?? {});
588
+ const mergedFileUrlEx = options.fileUrl ?? options.bootstrap?.fileUrl;
589
+ const mergedQaFileWireEx = options.qaFileWire ?? options.bootstrap?.qaFileWire;
590
+ if ((uploadEnabled && uploadStore) ||
591
+ (mergedFileUrlEx?.allowedHosts && mergedFileUrlEx.allowedHosts.length > 0)) {
592
+ try {
593
+ expressPayload = (await materializeExpressPayloadForInvoke({
594
+ socketId: socket.id,
595
+ uploadStore: uploadStore ?? null,
596
+ fileUrl: mergedFileUrlEx ?? null,
597
+ qaFileWire: mergedQaFileWireEx,
598
+ expressPayload,
599
+ }));
600
+ }
601
+ catch (err) {
602
+ const message = err instanceof Error ? err.message : 'error';
603
+ resp = rpcValidationError(requestId, fnKey, 'UPLOAD_MATERIALIZE_FAILED', message, start);
604
+ socket.emit('rpc:response', resp);
605
+ await hooks.onAfterRpc?.(socket, requestId, resp);
606
+ return;
607
+ }
608
+ }
609
+ const tEx = performance.now();
610
+ const preEx = await runPreInvokes({
611
+ fnKey,
612
+ args: [expressPayload],
613
+ socketId: socket.id,
614
+ transport: 'socket.io',
615
+ requestId,
616
+ authToken: readSocketAuthToken(socket),
617
+ });
618
+ if (preEx) {
619
+ socket.emit('rpc:response', preEx);
620
+ await hooks.onAfterRpc?.(socket, preEx.requestId, preEx);
621
+ await invokeAudit({
622
+ fnKey,
623
+ requestId: preEx.requestId,
624
+ status: preEx.status,
625
+ transport: 'socket.io',
626
+ socketId: socket.id,
627
+ durationMs: performance.now() - tEx,
628
+ });
629
+ return;
630
+ }
631
+ if (!invokeRateLimitAllow(socket.id)) {
632
+ resp = rpcValidationError(requestId, fnKey, 'RATE_LIMITED', `retry after ${invokeRateLimitRetryAfterMs(socket.id)}ms`, start);
633
+ socket.emit('rpc:response', resp);
634
+ await hooks.onAfterRpc?.(socket, requestId, resp);
635
+ await invokeAudit({
636
+ fnKey,
637
+ requestId,
638
+ status: 'error',
639
+ transport: 'socket.io',
640
+ socketId: socket.id,
641
+ durationMs: performance.now() - tEx,
642
+ });
643
+ return;
644
+ }
645
+ resp = await invokeExpressPlayground(requestId, fnKey, expressPayload, createMocks);
646
+ socket.emit('rpc:response', resp);
647
+ await hooks.onAfterRpc?.(socket, requestId, resp);
648
+ await invokeAudit({
649
+ fnKey,
650
+ requestId,
651
+ status: resp.status,
652
+ transport: 'socket.io',
653
+ socketId: socket.id,
654
+ durationMs: performance.now() - tEx,
655
+ });
656
+ return;
657
+ }
658
+ resp = rpcValidationError(requestId, fnKey, 'INVALID_PAYLOAD', 'kind must be service or express', start);
659
+ socket.emit('rpc:response', resp);
660
+ await hooks.onAfterRpc?.(socket, requestId, resp);
661
+ }
662
+ catch (e) {
663
+ const msg = e instanceof Error ? e.message : 'error';
664
+ const resp = rpcThrownError(rid, '', 'UNEXPECTED_ERROR', msg, start, 'unexpected', null);
665
+ socket.emit('rpc:response', resp);
666
+ await hooks.onAfterRpc?.(socket, resp.requestId, resp);
667
+ }
668
+ }, { source: 'rpc' });
669
+ });
670
+ // --- Binary upload (non-JSON) ---
671
+ if (uploadEnabled && uploadStore) {
672
+ socket.on('qa:upload:start', (raw) => {
673
+ try {
674
+ if (!raw || typeof raw !== 'object') {
675
+ socket.emit('qa:upload:error', { code: 'INVALID_PAYLOAD', message: 'expected object' });
676
+ return;
677
+ }
678
+ const p = raw;
679
+ const filename = typeof p.filename === 'string' ? p.filename : 'upload.bin';
680
+ const contentType = typeof p.contentType === 'string' ? p.contentType : 'application/octet-stream';
681
+ const sizeBytes = typeof p.sizeBytes === 'number' ? p.sizeBytes : NaN;
682
+ const uploadId = typeof p.uploadId === 'string' ? p.uploadId : undefined;
683
+ if (!Number.isFinite(sizeBytes) || sizeBytes < 0) {
684
+ socket.emit('qa:upload:error', {
685
+ uploadId: uploadId ?? null,
686
+ code: 'INVALID_PAYLOAD',
687
+ message: 'sizeBytes must be a number',
688
+ });
689
+ return;
690
+ }
691
+ const meta = uploadStore.start(socket.id, { uploadId, filename, contentType, sizeBytes });
692
+ socket.emit('qa:upload:ack', { uploadId: meta.uploadId, accepted: true });
693
+ }
694
+ catch (err) {
695
+ const message = err instanceof Error ? err.message : 'error';
696
+ socket.emit('qa:upload:error', { code: 'UPLOAD_START_FAILED', message });
697
+ }
698
+ });
699
+ socket.on('qa:upload:chunk', (chunk) => {
700
+ try {
701
+ const buf = Buffer.isBuffer(chunk)
702
+ ? chunk
703
+ : chunk instanceof ArrayBuffer
704
+ ? Buffer.from(chunk)
705
+ : null;
706
+ if (!buf) {
707
+ socket.emit('qa:upload:error', { code: 'INVALID_CHUNK', message: 'expected Buffer/ArrayBuffer' });
708
+ return;
709
+ }
710
+ const { uploadId, receivedBytes } = uploadStore.writeChunk(socket.id, buf);
711
+ socket.emit('qa:upload:progress', { uploadId, receivedBytes });
712
+ }
713
+ catch (err) {
714
+ const message = err instanceof Error ? err.message : 'error';
715
+ socket.emit('qa:upload:error', { code: 'UPLOAD_CHUNK_FAILED', message });
716
+ }
717
+ });
718
+ socket.on('qa:upload:finish', (raw) => {
719
+ try {
720
+ if (!raw || typeof raw !== 'object') {
721
+ socket.emit('qa:upload:error', { code: 'INVALID_PAYLOAD', message: 'expected object' });
722
+ return;
723
+ }
724
+ const p = raw;
725
+ const uploadId = typeof p.uploadId === 'string' ? p.uploadId : '';
726
+ if (!uploadId) {
727
+ socket.emit('qa:upload:error', { code: 'INVALID_PAYLOAD', message: 'uploadId required' });
728
+ return;
729
+ }
730
+ const done = uploadStore.finish(socket.id, uploadId);
731
+ socket.emit('qa:upload:complete', {
732
+ uploadId: done.uploadId,
733
+ sizeBytes: done.sizeBytes,
734
+ filename: done.filename,
735
+ contentType: done.contentType,
736
+ });
737
+ }
738
+ catch (err) {
739
+ const message = err instanceof Error ? err.message : 'error';
740
+ socket.emit('qa:upload:error', { code: 'UPLOAD_FINISH_FAILED', message });
741
+ }
742
+ });
743
+ socket.on('qa:upload:abort', () => {
744
+ try {
745
+ uploadStore.abort(socket.id);
746
+ socket.emit('qa:upload:ack', { aborted: true });
747
+ }
748
+ catch (err) {
749
+ const message = err instanceof Error ? err.message : 'error';
750
+ socket.emit('qa:upload:error', { code: 'UPLOAD_ABORT_FAILED', message });
751
+ }
752
+ });
753
+ socket.on('disconnect', () => {
754
+ try {
755
+ uploadStore.abort(socket.id);
756
+ }
757
+ catch {
758
+ // ignore
759
+ }
760
+ });
761
+ }
762
+ // --- Host Minio media over Socket (URL wire + host target + bootstrap.hostMinio) ---
763
+ if (qaHostMediaUploadViaSocket && hostMediaUploadStore && hostMinioResolved) {
764
+ const hm = hostMinioResolved;
765
+ socket.on('qa:hostMedia:start', (raw) => {
766
+ try {
767
+ if (!raw || typeof raw !== 'object') {
768
+ socket.emit('qa:hostMedia:error', { code: 'INVALID_PAYLOAD', message: 'expected object' });
769
+ return;
770
+ }
771
+ const p = raw;
772
+ const filename = typeof p.filename === 'string' ? p.filename : 'upload.bin';
773
+ const contentType = typeof p.contentType === 'string' ? p.contentType : 'application/octet-stream';
774
+ const sizeBytes = typeof p.sizeBytes === 'number' ? p.sizeBytes : NaN;
775
+ const uploadId = typeof p.uploadId === 'string' ? p.uploadId : undefined;
776
+ if (!Number.isFinite(sizeBytes) || sizeBytes < 0) {
777
+ socket.emit('qa:hostMedia:error', {
778
+ uploadId: uploadId ?? null,
779
+ code: 'INVALID_PAYLOAD',
780
+ message: 'sizeBytes must be a number',
781
+ });
782
+ return;
783
+ }
784
+ const meta = hostMediaUploadStore.start(socket.id, { uploadId, filename, contentType, sizeBytes });
785
+ socket.emit('qa:hostMedia:ack', { uploadId: meta.uploadId, accepted: true });
786
+ }
787
+ catch (err) {
788
+ const message = err instanceof Error ? err.message : 'error';
789
+ socket.emit('qa:hostMedia:error', { code: 'HOST_MEDIA_START_FAILED', message });
790
+ }
791
+ });
792
+ socket.on('qa:hostMedia:chunk', (chunk) => {
793
+ try {
794
+ const buf = Buffer.isBuffer(chunk)
795
+ ? chunk
796
+ : chunk instanceof ArrayBuffer
797
+ ? Buffer.from(chunk)
798
+ : null;
799
+ if (!buf) {
800
+ socket.emit('qa:hostMedia:error', { code: 'INVALID_CHUNK', message: 'expected Buffer/ArrayBuffer' });
801
+ return;
802
+ }
803
+ const { uploadId, receivedBytes } = hostMediaUploadStore.writeChunk(socket.id, buf);
804
+ socket.emit('qa:hostMedia:progress', { uploadId, receivedBytes });
805
+ }
806
+ catch (err) {
807
+ const message = err instanceof Error ? err.message : 'error';
808
+ socket.emit('qa:hostMedia:error', { code: 'HOST_MEDIA_CHUNK_FAILED', message });
809
+ }
810
+ });
811
+ socket.on('qa:hostMedia:finish', async (raw) => {
812
+ let finishUploadId = '';
813
+ try {
814
+ if (!raw || typeof raw !== 'object') {
815
+ socket.emit('qa:hostMedia:error', { code: 'INVALID_PAYLOAD', message: 'expected object' });
816
+ return;
817
+ }
818
+ const p = raw;
819
+ finishUploadId = typeof p.uploadId === 'string' ? p.uploadId : '';
820
+ if (!finishUploadId) {
821
+ socket.emit('qa:hostMedia:error', { code: 'INVALID_PAYLOAD', message: 'uploadId required' });
822
+ return;
823
+ }
824
+ const done = hostMediaUploadStore.finish(socket.id, finishUploadId);
825
+ const objectKey = `qa-host-media/${Date.now()}-${randomUUID()}`;
826
+ const { getUrl } = await putBufferAndPresignGetUrl(hm, {
827
+ objectKey,
828
+ buffer: done.buffer,
829
+ contentType: done.contentType || 'application/octet-stream',
830
+ });
831
+ socket.emit('qa:hostMedia:complete', { uploadId: done.uploadId, getUrl });
832
+ }
833
+ catch (err) {
834
+ const message = err instanceof Error ? err.message : 'error';
835
+ socket.emit('qa:hostMedia:error', {
836
+ code: 'HOST_MEDIA_FINISH_FAILED',
837
+ message,
838
+ uploadId: finishUploadId || null,
839
+ });
840
+ }
841
+ });
842
+ socket.on('qa:hostMedia:abort', () => {
843
+ try {
844
+ hostMediaUploadStore.abort(socket.id);
845
+ socket.emit('qa:hostMedia:ack', { aborted: true });
846
+ }
847
+ catch (err) {
848
+ const message = err instanceof Error ? err.message : 'error';
849
+ socket.emit('qa:hostMedia:error', { code: 'HOST_MEDIA_ABORT_FAILED', message });
850
+ }
851
+ });
852
+ socket.on('disconnect', () => {
853
+ try {
854
+ hostMediaUploadStore.abort(socket.id);
855
+ }
856
+ catch {
857
+ // ignore
858
+ }
859
+ });
860
+ }
861
+ })();
862
+ }
863
+ io.on('connection', onConnection);
864
+ return {
865
+ detach: () => {
866
+ unregisterRpcAuth?.();
867
+ configureInvokeRateLimit(null);
868
+ io.off('connection', onConnection);
869
+ if (sinkRegistered) {
870
+ clearBroadcastSink();
871
+ }
872
+ },
873
+ };
874
+ }
875
+ //# sourceMappingURL=socket-io-playground.js.map