statewire 0.14.0 → 0.15.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.
Files changed (94) hide show
  1. package/README.md +29 -80
  2. package/dist/client/connection.d.ts +19 -6
  3. package/dist/client/connection.d.ts.map +1 -1
  4. package/dist/client/connection.js +79 -40
  5. package/dist/client/connection.js.map +1 -1
  6. package/dist/client/core.d.ts +1 -0
  7. package/dist/client/core.d.ts.map +1 -1
  8. package/dist/client/core.js +5 -0
  9. package/dist/client/core.js.map +1 -1
  10. package/dist/client/session.d.ts +2 -0
  11. package/dist/client/session.d.ts.map +1 -1
  12. package/dist/client/session.js +96 -16
  13. package/dist/client/session.js.map +1 -1
  14. package/dist/host/adapter.d.ts +1 -1
  15. package/dist/host/adapter.d.ts.map +1 -1
  16. package/dist/host/adapter.js +7 -1
  17. package/dist/host/adapter.js.map +1 -1
  18. package/dist/host/host.d.ts +1 -0
  19. package/dist/host/host.d.ts.map +1 -1
  20. package/dist/host/host.js +45 -5
  21. package/dist/host/host.js.map +1 -1
  22. package/dist/host/http.d.ts +1 -1
  23. package/dist/host/http.d.ts.map +1 -1
  24. package/dist/host/http.js +75 -47
  25. package/dist/host/http.js.map +1 -1
  26. package/dist/host/socket.d.ts +23 -7
  27. package/dist/host/socket.d.ts.map +1 -1
  28. package/dist/host/socket.js +275 -74
  29. package/dist/host/socket.js.map +1 -1
  30. package/dist/host/work.d.ts +7 -0
  31. package/dist/host/work.d.ts.map +1 -0
  32. package/dist/host/work.js +53 -0
  33. package/dist/host/work.js.map +1 -0
  34. package/dist/host-internal.d.ts +35 -5
  35. package/dist/host-internal.d.ts.map +1 -1
  36. package/dist/host-internal.js +2 -1
  37. package/dist/host.d.ts +16 -3
  38. package/dist/host.d.ts.map +1 -1
  39. package/dist/host.js +2 -1
  40. package/dist/statewire.d.ts +11 -2
  41. package/dist/statewire.d.ts.map +1 -1
  42. package/dist/statewire.js.map +1 -1
  43. package/dist/transports/sse.d.ts +1 -4
  44. package/dist/transports/sse.d.ts.map +1 -1
  45. package/dist/transports/sse.js +47 -56
  46. package/dist/transports/sse.js.map +1 -1
  47. package/dist/transports/testing.js +2 -2
  48. package/dist/transports/testing.js.map +1 -1
  49. package/dist/transports/ws.d.ts +1 -1
  50. package/dist/transports/ws.d.ts.map +1 -1
  51. package/dist/transports/ws.js +54 -41
  52. package/dist/transports/ws.js.map +1 -1
  53. package/dist/types.d.ts +3 -0
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js.map +1 -1
  56. package/dist/wire/documents.d.ts +20 -0
  57. package/dist/wire/documents.d.ts.map +1 -0
  58. package/dist/wire/documents.js +74 -0
  59. package/dist/wire/documents.js.map +1 -0
  60. package/dist/wire/limits.d.ts +10 -0
  61. package/dist/wire/limits.d.ts.map +1 -0
  62. package/dist/wire/limits.js +11 -0
  63. package/dist/wire/limits.js.map +1 -0
  64. package/dist/wire/meta.d.ts.map +1 -1
  65. package/dist/wire/meta.js +1 -1
  66. package/dist/wire/meta.js.map +1 -1
  67. package/dist/wire/ops.d.ts.map +1 -1
  68. package/dist/wire/ops.js +1 -2
  69. package/dist/wire/ops.js.map +1 -1
  70. package/dist/wire/version.d.ts +7 -0
  71. package/dist/wire/version.d.ts.map +1 -0
  72. package/dist/wire/version.js +8 -0
  73. package/dist/wire/version.js.map +1 -0
  74. package/package.json +1 -1
  75. package/src/client/connection.ts +159 -52
  76. package/src/client/core.ts +6 -0
  77. package/src/client/session.ts +104 -29
  78. package/src/host/adapter.ts +10 -2
  79. package/src/host/host.ts +54 -4
  80. package/src/host/http.ts +132 -66
  81. package/src/host/socket.ts +410 -81
  82. package/src/host/work.ts +50 -0
  83. package/src/host-internal.ts +32 -4
  84. package/src/host.ts +18 -2
  85. package/src/statewire.ts +16 -2
  86. package/src/transports/sse.ts +60 -68
  87. package/src/transports/testing.ts +2 -2
  88. package/src/transports/ws.ts +29 -48
  89. package/src/types.ts +1 -0
  90. package/src/wire/documents.ts +109 -0
  91. package/src/wire/limits.ts +6 -0
  92. package/src/wire/meta.ts +6 -1
  93. package/src/wire/ops.ts +1 -2
  94. package/src/wire/version.ts +3 -0
package/src/host/host.ts CHANGED
@@ -136,6 +136,7 @@ export type CommandsHost = {
136
136
  submit?: (call: CommandsSubmission) => Promise<unknown>;
137
137
  /** Registered command names, read live. */
138
138
  methods?: () => string[];
139
+ abort?: () => void;
139
140
  };
140
141
 
141
142
  const commandsHosts = new WeakMap<object, CommandsHost>();
@@ -158,12 +159,15 @@ type QueuedCall = {
158
159
 
159
160
  type ActiveInvocation = CommandsInvocation & {
160
161
  applied: boolean;
162
+ controller: AbortController;
163
+ settled: boolean;
161
164
  origin?: CommandsSubmission;
162
165
  };
163
166
 
164
167
  type CommandsCell = {
165
168
  handlers: Record<string, (...args: never[]) => unknown> | null;
166
169
  queue: QueuedCall[];
170
+ active: Map<ActiveInvocation, QueuedCall>;
167
171
  running: boolean;
168
172
  listener: CommandsListener | null;
169
173
  seq: number;
@@ -178,6 +182,7 @@ const LOCAL_CALLER: StatewireHost.Caller = Object.freeze({
178
182
  get clientId(): string {
179
183
  throw new Error("statewire: in-process dispatch has no client id");
180
184
  },
185
+ auth: undefined,
181
186
  context: null,
182
187
  isConnected: true,
183
188
  });
@@ -202,6 +207,7 @@ const applyInvocation = (cell: CommandsCell, inv: ActiveInvocation) => {
202
207
  if (inv.applied) return;
203
208
  inv.applied = true;
204
209
  cell.listener?.onApplied(inv);
210
+ cell.listener?.onCovered?.(inv);
205
211
  inv.origin?.onApplied?.();
206
212
  };
207
213
 
@@ -211,6 +217,9 @@ const settleInvocation = (
211
217
  call: QueuedCall,
212
218
  settled: Settled,
213
219
  ) => {
220
+ if (inv.settled) return;
221
+ inv.settled = true;
222
+ cell.active.delete(inv);
214
223
  applyInvocation(cell, inv);
215
224
  if (settled.type === "resolved") {
216
225
  const outcome: CommandsOutcome =
@@ -229,13 +238,40 @@ const settleInvocation = (
229
238
  }
230
239
  };
231
240
 
241
+ const abortInvocations = (cell: CommandsCell) => {
242
+ for (const [inv, call] of cell.active) {
243
+ inv.settled = true;
244
+ inv.controller.abort();
245
+ const error = new StatewireSendError({ fate: "crashed", cause: "server" });
246
+ const outcome: CommandsOutcome = { type: "rejected", error };
247
+ cell.listener?.onSettle(inv, outcome);
248
+ inv.origin?.onSettle?.(outcome);
249
+ call.reject(error);
250
+ }
251
+ cell.active.clear();
252
+ };
253
+
232
254
  const isThenable = (value: unknown): value is PromiseLike<unknown> =>
233
255
  typeof value === "object" &&
234
256
  value !== null &&
235
257
  typeof (value as { then?: unknown }).then === "function";
236
258
 
259
+ // A command is an own, enumerable, function-valued property — exactly what
260
+ // `Object.keys` shows, so dispatch, `statewire/commands`, and the reserved-
261
+ // namespace guard share one view; prototype-chain names are not commands.
262
+ const handlerOf = (
263
+ handlers: Record<string, (...args: never[]) => unknown>,
264
+ method: string,
265
+ ): ((...args: never[]) => unknown) | undefined => {
266
+ if (!Object.prototype.propertyIsEnumerable.call(handlers, method)) {
267
+ return undefined;
268
+ }
269
+ const handler = handlers[method];
270
+ return typeof handler === "function" ? handler : undefined;
271
+ };
272
+
237
273
  const dispatch = (cell: CommandsCell, call: QueuedCall) => {
238
- const handler = cell.handlers![call.method];
274
+ const handler = handlerOf(cell.handlers!, call.method);
239
275
  if (handler === undefined) {
240
276
  const error = new StatewireSendError({
241
277
  fate: "rejected",
@@ -252,17 +288,22 @@ const dispatch = (cell: CommandsCell, call: QueuedCall) => {
252
288
  ts: call.ts,
253
289
  seq: ++cell.seq,
254
290
  applied: false,
291
+ controller: new AbortController(),
292
+ settled: false,
255
293
  ...(call.origin !== undefined && { origin: call.origin }),
256
294
  ...(call.origin?.clientId !== undefined && {
257
295
  clientId: call.origin.clientId,
258
296
  }),
259
297
  };
298
+ cell.active.set(inv, call);
260
299
  cell.listener?.onStart(inv);
261
300
  cell.listener?.onPosted?.(inv);
262
- cell.listener?.onCovered?.(inv);
263
301
  const ctx: StatewireHost.Ctx = {
264
302
  caller: call.origin?.caller ?? LOCAL_CALLER,
265
- applied: () => applyInvocation(cell, inv),
303
+ signal: inv.controller.signal,
304
+ applied: () => {
305
+ if (!inv.settled) applyInvocation(cell, inv);
306
+ },
266
307
  };
267
308
  try {
268
309
  const value = (
@@ -332,6 +373,7 @@ export const useStatewireCommands = <THandlers extends Statewire.Commands>(
332
373
  const cell: CommandsCell = {
333
374
  handlers: null,
334
375
  queue: [],
376
+ active: new Map(),
335
377
  running: false,
336
378
  listener: null,
337
379
  seq: 0,
@@ -358,10 +400,17 @@ export const useStatewireCommands = <THandlers extends Statewire.Commands>(
358
400
  }),
359
401
  );
360
402
  commandsHosts.set(proxy, {
403
+ abort: () => abortInvocations(cell),
361
404
  setListener: (listener) => {
362
405
  cell.listener = listener;
363
406
  },
364
- methods: () => (cell.handlers ? Object.keys(cell.handlers) : []),
407
+ methods: () => {
408
+ const handlers = cell.handlers;
409
+ if (!handlers) return [];
410
+ return Object.keys(handlers).filter(
411
+ (method) => handlerOf(handlers, method) !== undefined,
412
+ );
413
+ },
365
414
  submit: (origin) =>
366
415
  new Promise((resolve, reject) => {
367
416
  if (cell.unmounted) {
@@ -394,6 +443,7 @@ export const useStatewireCommands = <THandlers extends Statewire.Commands>(
394
443
  }
395
444
  queueMicrotask(() => {
396
445
  if (!cell.unmounted) return;
446
+ abortInvocations(cell);
397
447
  const queued = cell.queue;
398
448
  cell.queue = [];
399
449
  for (const call of queued) {
package/src/host/http.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  import type { StatewireHost } from "../host";
2
2
  import {
3
3
  CLIENT_ID_RE,
4
+ PRINCIPAL_MISMATCH_MESSAGE,
5
+ SUBSCRIBER_LIMIT_MESSAGE,
4
6
  admitFrame,
7
+ atSubscriberLimit,
8
+ authEquals,
5
9
  closeConn,
6
10
  flush,
7
11
  parseFrame,
@@ -10,14 +14,17 @@ import {
10
14
  sendTo,
11
15
  } from "./socket";
12
16
  import type { ParsedFrame, Registration, SocketHostState } from "./socket";
17
+ import { MAX_FRAME_BODY_BYTES } from "../wire/limits";
18
+ import { PROTOCOL_VERSION, VERSION_HEADER } from "../wire/version";
19
+
20
+ export { MAX_FRAME_BODY_BYTES } from "../wire/limits";
13
21
 
14
22
  const CLIENT_ID_HEADER = "Statewire-Client-Id";
15
23
  const LEASE_HEADER = "Statewire-Lease";
16
- const VERSION_HEADER = "Statewire-Version";
17
- const VERSION = "1";
18
24
  const encoder = new TextEncoder();
19
- export const MAX_FRAME_BODY_BYTES = 8 * 1024 * 1024;
20
-
25
+ const laggardFin = encoder.encode(
26
+ 'data: {"fin":{"reason":"reconnect","code":"client-fell-behind"}}\n\n',
27
+ );
21
28
  const readFrameBody = async (request: Request): Promise<string | null> => {
22
29
  const declared = request.headers.get("content-length");
23
30
  if (declared !== null && Number(declared) > MAX_FRAME_BODY_BYTES) {
@@ -83,30 +90,37 @@ const clientIdResponse = (
83
90
  const headersOf = (request: Request): Record<string, string> =>
84
91
  Object.fromEntries(request.headers);
85
92
 
86
- const mergedHeaders = (
87
- request: Request,
88
- extra: Record<string, string>,
89
- ): Record<string, string> => {
90
- const headers = headersOf(request);
91
- for (const [name, value] of Object.entries(extra)) {
92
- headers[name.toLowerCase()] = value;
93
- }
94
- return headers;
95
- };
96
-
97
93
  const authorize = async (
98
94
  h: SocketHostState,
99
95
  headers: Record<string, string>,
100
- ): Promise<Response | null> => {
96
+ ): Promise<{ auth: unknown } | { refused: Response }> => {
101
97
  try {
102
- await h.authorize?.({ headers });
103
- return null;
98
+ return { auth: await h.authorize?.({ headers }) };
104
99
  } catch (error) {
105
- if (error instanceof Response) return error;
100
+ if (error instanceof Response) return { refused: error };
106
101
  throw error;
107
102
  }
108
103
  };
109
104
 
105
+ const subscriberLimitResponse = (): Response =>
106
+ Response.json({ detail: SUBSCRIBER_LIMIT_MESSAGE }, { status: 503 });
107
+
108
+ // No awaits between this check and register: the binding cannot race.
109
+ const principalMismatchResponse = (
110
+ h: SocketHostState,
111
+ clientId: string,
112
+ auth: unknown,
113
+ ): Response | null => {
114
+ const rec = h.clients.get(clientId);
115
+ if (rec !== undefined && !authEquals(rec.principal, auth)) {
116
+ return Response.json(
117
+ { detail: PRINCIPAL_MISMATCH_MESSAGE },
118
+ { status: 403 },
119
+ );
120
+ }
121
+ return null;
122
+ };
123
+
110
124
  const mintLease = (): string => {
111
125
  const bytes = crypto.getRandomValues(new Uint8Array(16));
112
126
  return btoa(String.fromCharCode(...bytes))
@@ -118,24 +132,48 @@ const mintLease = (): string => {
118
132
  type SseAttachment = {
119
133
  body: ReadableStream<Uint8Array>;
120
134
  registration: Registration | null;
135
+ activateBackpressure: () => void;
121
136
  };
122
137
 
123
- const attachSse = (h: SocketHostState, clientId: string): SseAttachment => {
138
+ const attachSse = (
139
+ h: SocketHostState,
140
+ clientId: string,
141
+ auth: unknown,
142
+ ): SseAttachment => {
124
143
  let controller: ReadableStreamDefaultController<Uint8Array>;
125
144
  let connection: Registration["connection"] | null = null;
126
145
  let open = true;
127
- const body = new ReadableStream({
128
- start: (next) => {
129
- controller = next;
130
- },
131
- cancel: () => {
132
- open = false;
133
- connection?.close();
146
+ let backpressureActive = false;
147
+ const body = new ReadableStream(
148
+ {
149
+ start: (next) => {
150
+ controller = next;
151
+ },
152
+ cancel: () => {
153
+ open = false;
154
+ connection?.close();
155
+ },
134
156
  },
135
- });
157
+ { highWaterMark: h.streamQueueSize },
158
+ );
136
159
  const socket = {
137
160
  send: (data: string) => {
138
161
  if (!open) return;
162
+ if (
163
+ backpressureActive &&
164
+ controller.desiredSize !== null &&
165
+ controller.desiredSize <= 0
166
+ ) {
167
+ open = false;
168
+ try {
169
+ controller.enqueue(laggardFin);
170
+ controller.close();
171
+ } catch {
172
+ /* stream already gone */
173
+ }
174
+ connection?.close();
175
+ return;
176
+ }
139
177
  const frame = data === "{}" ? ": ping\n\n" : `data: ${data}\n\n`;
140
178
  try {
141
179
  controller.enqueue(encoder.encode(frame));
@@ -154,24 +192,32 @@ const attachSse = (h: SocketHostState, clientId: string): SseAttachment => {
154
192
  }
155
193
  },
156
194
  };
157
- const registration = register(h, clientId, socket);
195
+ const registration = register(h, clientId, socket, false, auth);
158
196
  connection = registration?.connection ?? null;
159
197
  if (!open) connection?.close();
160
- return { body, registration };
198
+ return {
199
+ body,
200
+ registration,
201
+ activateBackpressure: () => {
202
+ backpressureActive = true;
203
+ },
204
+ };
161
205
  };
162
206
 
163
207
  const streamResponse = (
164
208
  attachment: SseAttachment,
165
209
  lease: string | null,
166
- ): Response =>
167
- new Response(attachment.body, {
210
+ ): Response => {
211
+ attachment.activateBackpressure();
212
+ return new Response(attachment.body, {
168
213
  status: 200,
169
214
  headers: {
170
215
  "Content-Type": "text/event-stream",
171
- [VERSION_HEADER]: VERSION,
216
+ [VERSION_HEADER]: PROTOCOL_VERSION,
172
217
  ...(lease !== null && { [LEASE_HEADER]: lease }),
173
218
  },
174
219
  });
220
+ };
175
221
 
176
222
  const admitAttachFrame = (
177
223
  h: SocketHostState,
@@ -179,7 +225,12 @@ const admitAttachFrame = (
179
225
  frame: ParsedFrame,
180
226
  ) => {
181
227
  if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
182
- const admission = admitFrame(h, registration.rec, frame);
228
+ const admission = admitFrame(
229
+ h,
230
+ registration.rec,
231
+ frame,
232
+ registration.conn.auth,
233
+ );
183
234
  if (admission.type === "refused") {
184
235
  sendTo(h, registration.conn, JSON.stringify(admission.syn));
185
236
  }
@@ -189,11 +240,18 @@ const getStream = async (
189
240
  h: SocketHostState,
190
241
  request: Request,
191
242
  ): Promise<Response> => {
192
- const refused = await authorize(h, headersOf(request));
193
- if (refused !== null) return refused;
243
+ const authorized = await authorize(h, headersOf(request));
244
+ if ("refused" in authorized) return authorized.refused;
194
245
  const client = clientIdResponse(request);
195
246
  if ("response" in client) return client.response;
196
- const attachment = attachSse(h, client.clientId);
247
+ const mismatch = principalMismatchResponse(
248
+ h,
249
+ client.clientId,
250
+ authorized.auth,
251
+ );
252
+ if (mismatch !== null) return mismatch;
253
+ if (atSubscriberLimit(h)) return subscriberLimitResponse();
254
+ const attachment = attachSse(h, client.clientId, authorized.auth);
197
255
  const lease = attachment.registration === null ? null : mintLease();
198
256
  if (attachment.registration !== null)
199
257
  attachment.registration.conn.lease = lease;
@@ -212,28 +270,32 @@ const postStream = async (
212
270
  }
213
271
  let body: unknown;
214
272
  try {
215
- body = JSON.parse(raw);
273
+ body = raw.trim() ? JSON.parse(raw) : null;
216
274
  } catch {
217
275
  return Response.json({ detail: "body must be JSON" }, { status: 400 });
218
276
  }
219
277
  let frame: ParsedFrame;
220
278
  try {
221
- frame = parseFrameValue(body, true);
279
+ frame =
280
+ body === null && !raw.trim()
281
+ ? { ctx: [], cmd: [] }
282
+ : parseFrameValue(body);
222
283
  } catch (error) {
223
284
  if (!(error instanceof Error) || error.name !== "StatewireViolation") {
224
285
  throw error;
225
286
  }
226
287
  return Response.json({ detail: error.message }, { status: 400 });
227
288
  }
228
- if (frame.headers === null) {
229
- return Response.json(
230
- { detail: "first frame must carry headers" },
231
- { status: 400 },
232
- );
233
- }
234
- const refused = await authorize(h, mergedHeaders(request, frame.headers));
235
- if (refused !== null) return refused;
236
- const attachment = attachSse(h, client.clientId);
289
+ const authorized = await authorize(h, headersOf(request));
290
+ if ("refused" in authorized) return authorized.refused;
291
+ const mismatch = principalMismatchResponse(
292
+ h,
293
+ client.clientId,
294
+ authorized.auth,
295
+ );
296
+ if (mismatch !== null) return mismatch;
297
+ if (atSubscriberLimit(h)) return subscriberLimitResponse();
298
+ const attachment = attachSse(h, client.clientId, authorized.auth);
237
299
  if (attachment.registration === null) return streamResponse(attachment, null);
238
300
  const lease = mintLease();
239
301
  attachment.registration.conn.lease = lease;
@@ -272,8 +334,8 @@ const violationResponse = (
272
334
  reason: string,
273
335
  ) => {
274
336
  finClient(h, clientId, {
275
- reason: "error",
276
- message: `protocol violation: ${reason}`,
337
+ reason: "protocol-error",
338
+ message: reason,
277
339
  });
278
340
  return Response.json({ error: reason }, { status: 400 });
279
341
  };
@@ -282,38 +344,42 @@ export const frames = async (
282
344
  h: SocketHostState,
283
345
  request: Request,
284
346
  ): Promise<Response> => {
285
- const refused = await authorize(h, headersOf(request));
286
- if (refused !== null) return refused;
287
- const client = clientIdResponse(request);
288
- if ("response" in client) return client.response;
289
- const rec = h.clients.get(client.clientId);
290
347
  const lease = request.headers.get(LEASE_HEADER);
291
348
  // A lease lives and dies with its attach: any live attach of the id accepts.
292
- const live =
293
- lease !== null &&
294
- [...h.conns].some(
295
- (conn) =>
296
- conn.open && conn.clientId === client.clientId && conn.lease === lease,
297
- );
298
- if (h.disposed || h.finished !== null || rec === undefined || !live) {
349
+ const leased =
350
+ lease === null
351
+ ? undefined
352
+ : [...h.conns].find((conn) => conn.open && conn.lease === lease);
353
+ if (h.disposed || h.finished !== null || leased === undefined) {
299
354
  return Response.json({ error: "stale-lease" }, { status: 423 });
300
355
  }
356
+ const clientId = leased.clientId;
357
+ const rec = h.clients.get(clientId)!;
301
358
  const raw = await readFrameBody(request);
302
359
  if (raw === null) {
303
360
  return Response.json({ error: "frame body too large" }, { status: 413 });
304
361
  }
305
362
  let frame: ParsedFrame;
306
363
  try {
307
- frame = parseFrame(raw, false);
364
+ frame = parseFrame(raw);
308
365
  } catch (error) {
309
366
  if (!(error instanceof Error) || error.name !== "StatewireViolation") {
310
367
  throw error;
311
368
  }
312
- return violationResponse(h, client.clientId, error.message);
369
+ return violationResponse(h, clientId, error.message);
370
+ }
371
+ if (!leased.open || h.disposed || h.finished !== null) {
372
+ return Response.json({ error: "stale-lease" }, { status: 423 });
313
373
  }
314
- const admission = admitFrame(h, rec, frame);
374
+ const admission = admitFrame(h, rec, frame, leased.auth);
315
375
  if (admission.type === "refused") {
316
- return Response.json(admission.syn, { status: 409 });
376
+ const retry = admission.syn.syn.retry;
377
+ return Response.json(admission.syn, {
378
+ status: retry === undefined ? 409 : 429,
379
+ ...(retry !== undefined && {
380
+ headers: { "Retry-After": String(Math.ceil(retry / 1000)) },
381
+ }),
382
+ });
317
383
  }
318
384
  return Response.json({});
319
385
  };