experimental-a2 0.9.0 → 0.10.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.
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * The A2 socket protocol's server half (specs/a2-api.md §13): the
3
3
  * per-socket shell — teardown, backpressure, heartbeat, the malformed
4
- * budget, the in-flight bound — and `sessionsSocket`, the multiplexed
5
- * frame handler riding it. Internal module: `handle`'s upgrade lane is
6
- * the sanctioned mount; only `A2Socket` (the structural socket the
7
- * platform hands over) is re-exported as public surface.
4
+ * budget, and the in-flight bound. `sessionSocket` binds one session to
5
+ * one socket; `sessionsSocket` is the opt-in multiplexed handler. Only
6
+ * `A2Socket`, the structural socket the platform hands over, is public.
8
7
  */
9
8
 
10
9
  import { A2Error } from './errors.ts'
@@ -256,17 +255,210 @@ export type SessionsSocketOptions = {
256
255
  ): A2Error | null | Promise<A2Error | null>
257
256
  }
258
257
 
258
+ export type SessionSocketOptions = SessionsSocketOptions & {
259
+ startAfter?: number
260
+ }
261
+
259
262
  type SocketSubscription = {
260
263
  target: SocketSession
261
264
  iterator: AsyncIterator<Event | PresencePatch | PresenceSnapshot>
262
265
  stopped: boolean
263
266
  }
264
267
 
268
+ const socketSubscription = (
269
+ target: SocketSession,
270
+ startAfter: number,
271
+ presence: boolean,
272
+ ): SocketSubscription => ({
273
+ target,
274
+ iterator: (presence
275
+ ? target.stream({ startAfter, presence: true })
276
+ : target.stream({ startAfter }))[Symbol.asyncIterator](),
277
+ stopped: false,
278
+ })
279
+
265
280
  const stopSubscription = (subscription: SocketSubscription): void => {
266
281
  subscription.stopped = true
267
282
  void subscription.iterator.return?.()?.catch(() => {})
268
283
  }
269
284
 
285
+ const pumpSubscription = async (
286
+ shell: SocketShell,
287
+ subscription: SocketSubscription,
288
+ frameFor: (item: Event | PresencePatch | PresenceSnapshot) => string,
289
+ ): Promise<void> => {
290
+ for (;;) {
291
+ while (
292
+ !shell.torn() &&
293
+ !subscription.stopped &&
294
+ shell.buffered() > SOCKET_TIMINGS.highWaterMarkBytes
295
+ ) {
296
+ // oxlint-disable-next-line no-await-in-loop -- backpressure park
297
+ await new Promise((wake) => setTimeout(wake, SOCKET_TIMINGS.resumePollMs))
298
+ }
299
+ if (shell.torn() || subscription.stopped) return
300
+ // oxlint-disable-next-line no-await-in-loop -- stream pump
301
+ const { value, done } = await subscription.iterator.next()
302
+ if (shell.torn() || subscription.stopped || done) return
303
+ shell.send(frameFor(value))
304
+ }
305
+ }
306
+
307
+ const handleSocketPush = async (
308
+ shell: SocketShell,
309
+ target: SocketSession,
310
+ sessionId: string,
311
+ frame: Record<string, unknown>,
312
+ req: number,
313
+ ackSessionId: string | undefined,
314
+ gatePush: SessionsSocketOptions['gatePush'],
315
+ ): Promise<void> => {
316
+ try {
317
+ const events = parsePushEvents(frame['events'])
318
+ const denial = (await gatePush?.(sessionId, events, undefined)) ?? null
319
+ if (denial !== null) {
320
+ shell.send(socketErrorAckFor(req, denial, ackSessionId))
321
+ return
322
+ }
323
+ // oxlint-disable-next-line a2/await-handler-append -- this transport append is awaited before its ack
324
+ const appended = await target.append(...events)
325
+ shell.send(socketAckFor(req, appended, ackSessionId))
326
+ } catch (error) {
327
+ shell.send(socketErrorAckFor(req, asA2Error(error), ackSessionId))
328
+ }
329
+ }
330
+
331
+ const applySocketPresence = async (
332
+ shell: SocketShell,
333
+ target: SocketSession,
334
+ sessionId: string,
335
+ frame: Record<string, unknown>,
336
+ gatePush: SessionsSocketOptions['gatePush'],
337
+ ): Promise<void> => {
338
+ if (typeof target.setPresence !== 'function') {
339
+ shell.malformedFrame('presence frame on a presence-less session')
340
+ return
341
+ }
342
+ let patch: ParsedPushPresence | undefined
343
+ try {
344
+ patch = parsePresenceSibling(frame)
345
+ } catch {
346
+ shell.malformedFrame('invalid presence frame')
347
+ return
348
+ }
349
+ if (!patch) return
350
+ try {
351
+ const denial = (await gatePush?.(sessionId, [], patch)) ?? null
352
+ if (denial !== null) return
353
+ await target.setPresence(patch)
354
+ } catch (error) {
355
+ const failure = asA2Error(error)
356
+ if (
357
+ failure.code === 'INVALID_PAYLOAD' ||
358
+ failure.code === 'UNKNOWN_PRESENCE_FIELD'
359
+ ) {
360
+ shell.malformedFrame('invalid presence frame')
361
+ return
362
+ }
363
+ shell.shutdown(1011, 'internal error')
364
+ }
365
+ }
366
+
367
+ export function sessionSocket<D extends EventDefs>(
368
+ sessionId: string,
369
+ session: Pick<Session<D>, 'stream'>,
370
+ socket: A2Socket,
371
+ options?: SessionSocketOptions,
372
+ ): void {
373
+ const target = session as unknown as SocketSession
374
+ const subscription = socketSubscription(
375
+ target,
376
+ options?.startAfter ?? 0,
377
+ options?.presence === true,
378
+ )
379
+
380
+ const handleFrame = (frame: Record<string, unknown>): void => {
381
+ switch (frame['kind']) {
382
+ case 'push': {
383
+ const req = frame['req']
384
+ if (typeof req !== 'number') {
385
+ shell.malformedFrame('push frame without a numeric req')
386
+ return
387
+ }
388
+ if (frame['sessionId'] !== undefined) {
389
+ shell.send(
390
+ socketErrorAckFor(
391
+ req,
392
+ new A2Error(
393
+ 'INVALID_PAYLOAD',
394
+ 'sessionId is not allowed on a single-session socket',
395
+ ),
396
+ ),
397
+ )
398
+ return
399
+ }
400
+ if (shell.overCap()) {
401
+ shell.send(
402
+ socketErrorAckFor(
403
+ req,
404
+ new A2Error(
405
+ 'STORE_UNAVAILABLE',
406
+ 'push shed: too many in flight on this socket',
407
+ ),
408
+ ),
409
+ )
410
+ return
411
+ }
412
+ shell.track(
413
+ handleSocketPush(
414
+ shell,
415
+ target,
416
+ sessionId,
417
+ frame,
418
+ req,
419
+ undefined,
420
+ options?.gatePush,
421
+ ),
422
+ )
423
+ return
424
+ }
425
+ case 'presence':
426
+ if (frame['sessionId'] !== undefined) {
427
+ shell.malformedFrame(
428
+ 'sessionId is not allowed on a single-session socket',
429
+ )
430
+ return
431
+ }
432
+ if (!shell.overCap()) {
433
+ shell.track(
434
+ applySocketPresence(
435
+ shell,
436
+ target,
437
+ sessionId,
438
+ frame,
439
+ options?.gatePush,
440
+ ),
441
+ )
442
+ }
443
+ return
444
+ default:
445
+ return
446
+ }
447
+ }
448
+
449
+ const shell = socketShell(
450
+ socket,
451
+ () => stopSubscription(subscription),
452
+ handleFrame,
453
+ options?.deadline,
454
+ )
455
+
456
+ void pumpSubscription(shell, subscription, socketFrameFor).then(
457
+ () => shell.shutdown(1000),
458
+ () => shell.shutdown(1011, 'stream failed'),
459
+ )
460
+ }
461
+
270
462
  const parseSubscribeEntries = (
271
463
  value: unknown,
272
464
  ): Array<{ id: string; index: number }> | null => {
@@ -308,38 +500,14 @@ export function sessionsSocket<D extends EventDefs>(
308
500
  if (subscriptions.get(id) === subscription) subscriptions.delete(id)
309
501
  }
310
502
 
311
- const pump = async (
312
- id: string,
313
- subscription: SocketSubscription,
314
- ): Promise<void> => {
315
- for (;;) {
316
- for (;;) {
317
- if (
318
- shell.torn() ||
319
- subscription.stopped ||
320
- shell.buffered() <= SOCKET_TIMINGS.highWaterMarkBytes
321
- )
322
- break
323
- // oxlint-disable-next-line no-await-in-loop -- backpressure park
324
- await new Promise((wake) =>
325
- setTimeout(wake, SOCKET_TIMINGS.resumePollMs),
326
- )
327
- }
328
- if (shell.torn() || subscription.stopped) return
329
- // oxlint-disable-next-line no-await-in-loop -- stream pump
330
- const { value, done } = await subscription.iterator.next()
331
- if (shell.torn() || subscription.stopped) return
332
- if (done) return
333
- shell.send(socketFrameFor(value, id))
334
- }
335
- }
336
-
337
503
  const runSubscription = async (
338
504
  id: string,
339
505
  subscription: SocketSubscription,
340
506
  ): Promise<void> => {
341
507
  try {
342
- await pump(id, subscription)
508
+ await pumpSubscription(shell, subscription, (item) =>
509
+ socketFrameFor(item, id),
510
+ )
343
511
  if (shell.torn() || subscription.stopped) return
344
512
  retire(id, subscription)
345
513
  shell.send(socketUnsubscribedFor(id))
@@ -371,16 +539,7 @@ export function sessionsSocket<D extends EventDefs>(
371
539
  const target = session as unknown as SocketSession
372
540
  const existing = subscriptions.get(id)
373
541
  if (existing) stopSubscription(existing)
374
- const iterator = (
375
- presence
376
- ? target.stream({ startAfter: index, presence: true })
377
- : target.stream({ startAfter: index })
378
- )[Symbol.asyncIterator]()
379
- const subscription: SocketSubscription = {
380
- target,
381
- iterator,
382
- stopped: false,
383
- }
542
+ const subscription = socketSubscription(target, index, presence)
384
543
  subscriptions.set(id, subscription)
385
544
  shell.send(socketSubscribedFor(id))
386
545
  void runSubscription(id, subscription)
@@ -415,19 +574,15 @@ export function sessionsSocket<D extends EventDefs>(
415
574
  )
416
575
  return
417
576
  }
418
- try {
419
- const events = parsePushEvents(frame['events'])
420
- const denial =
421
- (await options?.gatePush?.(sessionId, events, undefined)) ?? null
422
- if (denial !== null) {
423
- shell.send(socketErrorAckFor(req, denial, sessionId))
424
- return
425
- }
426
- const appended = await subscription.target.append(...events)
427
- shell.send(socketAckFor(req, appended, sessionId))
428
- } catch (err) {
429
- shell.send(socketErrorAckFor(req, asA2Error(err), sessionId))
430
- }
577
+ await handleSocketPush(
578
+ shell,
579
+ subscription.target,
580
+ sessionId,
581
+ frame,
582
+ req,
583
+ sessionId,
584
+ options?.gatePush,
585
+ )
431
586
  }
432
587
 
433
588
  const handlePresence = async (
@@ -437,33 +592,13 @@ export function sessionsSocket<D extends EventDefs>(
437
592
  const subscription = subscriptions.get(sessionId)
438
593
  // An unsubscribe race, not malformedness — the plane repaints.
439
594
  if (subscription === undefined) return
440
- if (typeof subscription.target.setPresence !== 'function') {
441
- shell.malformedFrame('presence frame on a presence-less session')
442
- return
443
- }
444
- let patch: ParsedPushPresence | undefined
445
- try {
446
- patch = parsePresenceSibling(frame)
447
- } catch {
448
- shell.malformedFrame('invalid presence frame')
449
- return
450
- }
451
- if (!patch) return
452
- try {
453
- const denial = (await options?.gatePush?.(sessionId, [], patch)) ?? null
454
- if (denial !== null) return
455
- await subscription.target.setPresence(patch)
456
- } catch (error) {
457
- const failure = asA2Error(error)
458
- if (
459
- failure.code === 'INVALID_PAYLOAD' ||
460
- failure.code === 'UNKNOWN_PRESENCE_FIELD'
461
- ) {
462
- shell.malformedFrame('invalid presence frame')
463
- return
464
- }
465
- shell.shutdown(1011, 'internal error')
466
- }
595
+ await applySocketPresence(
596
+ shell,
597
+ subscription.target,
598
+ sessionId,
599
+ frame,
600
+ options?.gatePush,
601
+ )
467
602
  }
468
603
 
469
604
  const handleFrame = (frame: Record<string, unknown>): void => {