@atproto/bsync 0.0.3 → 0.0.5

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 (80) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/buf.gen.yaml +2 -2
  3. package/dist/context.d.ts +2 -0
  4. package/dist/context.d.ts.map +1 -1
  5. package/dist/context.js +1 -0
  6. package/dist/context.js.map +1 -1
  7. package/dist/db/migrations/20240717T224303472Z-notif-ops.d.ts +4 -0
  8. package/dist/db/migrations/20240717T224303472Z-notif-ops.d.ts.map +1 -0
  9. package/dist/db/migrations/20240717T224303472Z-notif-ops.js +26 -0
  10. package/dist/db/migrations/20240717T224303472Z-notif-ops.js.map +1 -0
  11. package/dist/db/migrations/index.d.ts +1 -0
  12. package/dist/db/migrations/index.d.ts.map +1 -1
  13. package/dist/db/migrations/index.js +2 -1
  14. package/dist/db/migrations/index.js.map +1 -1
  15. package/dist/db/schema/index.d.ts +3 -1
  16. package/dist/db/schema/index.d.ts.map +1 -1
  17. package/dist/db/schema/notif_item.d.ts +12 -0
  18. package/dist/db/schema/notif_item.d.ts.map +1 -0
  19. package/dist/db/schema/notif_item.js +5 -0
  20. package/dist/db/schema/notif_item.js.map +1 -0
  21. package/dist/db/schema/notif_op.d.ts +14 -0
  22. package/dist/db/schema/notif_op.d.ts.map +1 -0
  23. package/dist/db/schema/notif_op.js +6 -0
  24. package/dist/db/schema/notif_op.js.map +1 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +8 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/logger.d.ts +4 -1
  29. package/dist/logger.d.ts.map +1 -1
  30. package/dist/logger.js +31 -8
  31. package/dist/logger.js.map +1 -1
  32. package/dist/proto/bsync_connect.d.ts +19 -1
  33. package/dist/proto/bsync_connect.d.ts.map +1 -1
  34. package/dist/proto/bsync_connect.js +19 -1
  35. package/dist/proto/bsync_connect.js.map +1 -1
  36. package/dist/proto/bsync_pb.d.ts +105 -0
  37. package/dist/proto/bsync_pb.d.ts.map +1 -1
  38. package/dist/proto/bsync_pb.js +325 -2
  39. package/dist/proto/bsync_pb.js.map +1 -1
  40. package/dist/routes/add-mute-operation.d.ts.map +1 -1
  41. package/dist/routes/add-mute-operation.js +7 -26
  42. package/dist/routes/add-mute-operation.js.map +1 -1
  43. package/dist/routes/add-notif-operation.d.ts +6 -0
  44. package/dist/routes/add-notif-operation.d.ts.map +1 -0
  45. package/dist/routes/add-notif-operation.js +63 -0
  46. package/dist/routes/add-notif-operation.js.map +1 -0
  47. package/dist/routes/index.d.ts.map +1 -1
  48. package/dist/routes/index.js +4 -0
  49. package/dist/routes/index.js.map +1 -1
  50. package/dist/routes/scan-mute-operations.d.ts.map +1 -1
  51. package/dist/routes/scan-mute-operations.js +3 -26
  52. package/dist/routes/scan-mute-operations.js.map +1 -1
  53. package/dist/routes/scan-notif-operations.d.ts +6 -0
  54. package/dist/routes/scan-notif-operations.d.ts.map +1 -0
  55. package/dist/routes/scan-notif-operations.js +56 -0
  56. package/dist/routes/scan-notif-operations.js.map +1 -0
  57. package/dist/routes/util.d.ts +6 -0
  58. package/dist/routes/util.d.ts.map +1 -0
  59. package/dist/routes/util.js +54 -0
  60. package/dist/routes/util.js.map +1 -0
  61. package/package.json +3 -2
  62. package/proto/bsync.proto +29 -0
  63. package/src/context.ts +2 -0
  64. package/src/db/migrations/20240717T224303472Z-notif-ops.ts +24 -0
  65. package/src/db/migrations/index.ts +1 -0
  66. package/src/db/schema/index.ts +6 -1
  67. package/src/db/schema/notif_item.ts +13 -0
  68. package/src/db/schema/notif_op.ts +16 -0
  69. package/src/index.ts +8 -2
  70. package/src/logger.ts +11 -7
  71. package/src/proto/bsync_connect.ts +23 -1
  72. package/src/proto/bsync_pb.ts +318 -1
  73. package/src/routes/add-mute-operation.ts +7 -29
  74. package/src/routes/add-notif-operation.ts +80 -0
  75. package/src/routes/index.ts +4 -0
  76. package/src/routes/scan-mute-operations.ts +2 -25
  77. package/src/routes/scan-notif-operations.ts +64 -0
  78. package/src/routes/util.ts +51 -0
  79. package/tests/mutes.test.ts +2 -0
  80. package/tests/notifications.test.ts +209 -0
@@ -1,4 +1,4 @@
1
- // @generated by protoc-gen-es v1.6.0 with parameter "target=ts,import_extension=.ts"
1
+ // @generated by protoc-gen-es v1.6.0 with parameter "target=ts,import_extension="
2
2
  // @generated from file bsync.proto (package bsync, syntax proto3)
3
3
  /* eslint-disable */
4
4
  // @ts-nocheck
@@ -372,6 +372,323 @@ export class ScanMuteOperationsResponse extends Message<ScanMuteOperationsRespon
372
372
  }
373
373
  }
374
374
 
375
+ /**
376
+ * @generated from message bsync.NotifOperation
377
+ */
378
+ export class NotifOperation extends Message<NotifOperation> {
379
+ /**
380
+ * @generated from field: string id = 1;
381
+ */
382
+ id = ''
383
+
384
+ /**
385
+ * @generated from field: string actor_did = 2;
386
+ */
387
+ actorDid = ''
388
+
389
+ /**
390
+ * @generated from field: optional bool priority = 3;
391
+ */
392
+ priority?: boolean
393
+
394
+ constructor(data?: PartialMessage<NotifOperation>) {
395
+ super()
396
+ proto3.util.initPartial(data, this)
397
+ }
398
+
399
+ static readonly runtime: typeof proto3 = proto3
400
+ static readonly typeName = 'bsync.NotifOperation'
401
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
402
+ { no: 1, name: 'id', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
403
+ { no: 2, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
404
+ {
405
+ no: 3,
406
+ name: 'priority',
407
+ kind: 'scalar',
408
+ T: 8 /* ScalarType.BOOL */,
409
+ opt: true,
410
+ },
411
+ ])
412
+
413
+ static fromBinary(
414
+ bytes: Uint8Array,
415
+ options?: Partial<BinaryReadOptions>,
416
+ ): NotifOperation {
417
+ return new NotifOperation().fromBinary(bytes, options)
418
+ }
419
+
420
+ static fromJson(
421
+ jsonValue: JsonValue,
422
+ options?: Partial<JsonReadOptions>,
423
+ ): NotifOperation {
424
+ return new NotifOperation().fromJson(jsonValue, options)
425
+ }
426
+
427
+ static fromJsonString(
428
+ jsonString: string,
429
+ options?: Partial<JsonReadOptions>,
430
+ ): NotifOperation {
431
+ return new NotifOperation().fromJsonString(jsonString, options)
432
+ }
433
+
434
+ static equals(
435
+ a: NotifOperation | PlainMessage<NotifOperation> | undefined,
436
+ b: NotifOperation | PlainMessage<NotifOperation> | undefined,
437
+ ): boolean {
438
+ return proto3.util.equals(NotifOperation, a, b)
439
+ }
440
+ }
441
+
442
+ /**
443
+ * @generated from message bsync.AddNotifOperationRequest
444
+ */
445
+ export class AddNotifOperationRequest extends Message<AddNotifOperationRequest> {
446
+ /**
447
+ * @generated from field: string actor_did = 1;
448
+ */
449
+ actorDid = ''
450
+
451
+ /**
452
+ * @generated from field: optional bool priority = 2;
453
+ */
454
+ priority?: boolean
455
+
456
+ constructor(data?: PartialMessage<AddNotifOperationRequest>) {
457
+ super()
458
+ proto3.util.initPartial(data, this)
459
+ }
460
+
461
+ static readonly runtime: typeof proto3 = proto3
462
+ static readonly typeName = 'bsync.AddNotifOperationRequest'
463
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
464
+ { no: 1, name: 'actor_did', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
465
+ {
466
+ no: 2,
467
+ name: 'priority',
468
+ kind: 'scalar',
469
+ T: 8 /* ScalarType.BOOL */,
470
+ opt: true,
471
+ },
472
+ ])
473
+
474
+ static fromBinary(
475
+ bytes: Uint8Array,
476
+ options?: Partial<BinaryReadOptions>,
477
+ ): AddNotifOperationRequest {
478
+ return new AddNotifOperationRequest().fromBinary(bytes, options)
479
+ }
480
+
481
+ static fromJson(
482
+ jsonValue: JsonValue,
483
+ options?: Partial<JsonReadOptions>,
484
+ ): AddNotifOperationRequest {
485
+ return new AddNotifOperationRequest().fromJson(jsonValue, options)
486
+ }
487
+
488
+ static fromJsonString(
489
+ jsonString: string,
490
+ options?: Partial<JsonReadOptions>,
491
+ ): AddNotifOperationRequest {
492
+ return new AddNotifOperationRequest().fromJsonString(jsonString, options)
493
+ }
494
+
495
+ static equals(
496
+ a:
497
+ | AddNotifOperationRequest
498
+ | PlainMessage<AddNotifOperationRequest>
499
+ | undefined,
500
+ b:
501
+ | AddNotifOperationRequest
502
+ | PlainMessage<AddNotifOperationRequest>
503
+ | undefined,
504
+ ): boolean {
505
+ return proto3.util.equals(AddNotifOperationRequest, a, b)
506
+ }
507
+ }
508
+
509
+ /**
510
+ * @generated from message bsync.AddNotifOperationResponse
511
+ */
512
+ export class AddNotifOperationResponse extends Message<AddNotifOperationResponse> {
513
+ /**
514
+ * @generated from field: bsync.NotifOperation operation = 1;
515
+ */
516
+ operation?: NotifOperation
517
+
518
+ constructor(data?: PartialMessage<AddNotifOperationResponse>) {
519
+ super()
520
+ proto3.util.initPartial(data, this)
521
+ }
522
+
523
+ static readonly runtime: typeof proto3 = proto3
524
+ static readonly typeName = 'bsync.AddNotifOperationResponse'
525
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
526
+ { no: 1, name: 'operation', kind: 'message', T: NotifOperation },
527
+ ])
528
+
529
+ static fromBinary(
530
+ bytes: Uint8Array,
531
+ options?: Partial<BinaryReadOptions>,
532
+ ): AddNotifOperationResponse {
533
+ return new AddNotifOperationResponse().fromBinary(bytes, options)
534
+ }
535
+
536
+ static fromJson(
537
+ jsonValue: JsonValue,
538
+ options?: Partial<JsonReadOptions>,
539
+ ): AddNotifOperationResponse {
540
+ return new AddNotifOperationResponse().fromJson(jsonValue, options)
541
+ }
542
+
543
+ static fromJsonString(
544
+ jsonString: string,
545
+ options?: Partial<JsonReadOptions>,
546
+ ): AddNotifOperationResponse {
547
+ return new AddNotifOperationResponse().fromJsonString(jsonString, options)
548
+ }
549
+
550
+ static equals(
551
+ a:
552
+ | AddNotifOperationResponse
553
+ | PlainMessage<AddNotifOperationResponse>
554
+ | undefined,
555
+ b:
556
+ | AddNotifOperationResponse
557
+ | PlainMessage<AddNotifOperationResponse>
558
+ | undefined,
559
+ ): boolean {
560
+ return proto3.util.equals(AddNotifOperationResponse, a, b)
561
+ }
562
+ }
563
+
564
+ /**
565
+ * @generated from message bsync.ScanNotifOperationsRequest
566
+ */
567
+ export class ScanNotifOperationsRequest extends Message<ScanNotifOperationsRequest> {
568
+ /**
569
+ * @generated from field: string cursor = 1;
570
+ */
571
+ cursor = ''
572
+
573
+ /**
574
+ * @generated from field: int32 limit = 2;
575
+ */
576
+ limit = 0
577
+
578
+ constructor(data?: PartialMessage<ScanNotifOperationsRequest>) {
579
+ super()
580
+ proto3.util.initPartial(data, this)
581
+ }
582
+
583
+ static readonly runtime: typeof proto3 = proto3
584
+ static readonly typeName = 'bsync.ScanNotifOperationsRequest'
585
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
586
+ { no: 1, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
587
+ { no: 2, name: 'limit', kind: 'scalar', T: 5 /* ScalarType.INT32 */ },
588
+ ])
589
+
590
+ static fromBinary(
591
+ bytes: Uint8Array,
592
+ options?: Partial<BinaryReadOptions>,
593
+ ): ScanNotifOperationsRequest {
594
+ return new ScanNotifOperationsRequest().fromBinary(bytes, options)
595
+ }
596
+
597
+ static fromJson(
598
+ jsonValue: JsonValue,
599
+ options?: Partial<JsonReadOptions>,
600
+ ): ScanNotifOperationsRequest {
601
+ return new ScanNotifOperationsRequest().fromJson(jsonValue, options)
602
+ }
603
+
604
+ static fromJsonString(
605
+ jsonString: string,
606
+ options?: Partial<JsonReadOptions>,
607
+ ): ScanNotifOperationsRequest {
608
+ return new ScanNotifOperationsRequest().fromJsonString(jsonString, options)
609
+ }
610
+
611
+ static equals(
612
+ a:
613
+ | ScanNotifOperationsRequest
614
+ | PlainMessage<ScanNotifOperationsRequest>
615
+ | undefined,
616
+ b:
617
+ | ScanNotifOperationsRequest
618
+ | PlainMessage<ScanNotifOperationsRequest>
619
+ | undefined,
620
+ ): boolean {
621
+ return proto3.util.equals(ScanNotifOperationsRequest, a, b)
622
+ }
623
+ }
624
+
625
+ /**
626
+ * @generated from message bsync.ScanNotifOperationsResponse
627
+ */
628
+ export class ScanNotifOperationsResponse extends Message<ScanNotifOperationsResponse> {
629
+ /**
630
+ * @generated from field: repeated bsync.NotifOperation operations = 1;
631
+ */
632
+ operations: NotifOperation[] = []
633
+
634
+ /**
635
+ * @generated from field: string cursor = 2;
636
+ */
637
+ cursor = ''
638
+
639
+ constructor(data?: PartialMessage<ScanNotifOperationsResponse>) {
640
+ super()
641
+ proto3.util.initPartial(data, this)
642
+ }
643
+
644
+ static readonly runtime: typeof proto3 = proto3
645
+ static readonly typeName = 'bsync.ScanNotifOperationsResponse'
646
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
647
+ {
648
+ no: 1,
649
+ name: 'operations',
650
+ kind: 'message',
651
+ T: NotifOperation,
652
+ repeated: true,
653
+ },
654
+ { no: 2, name: 'cursor', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
655
+ ])
656
+
657
+ static fromBinary(
658
+ bytes: Uint8Array,
659
+ options?: Partial<BinaryReadOptions>,
660
+ ): ScanNotifOperationsResponse {
661
+ return new ScanNotifOperationsResponse().fromBinary(bytes, options)
662
+ }
663
+
664
+ static fromJson(
665
+ jsonValue: JsonValue,
666
+ options?: Partial<JsonReadOptions>,
667
+ ): ScanNotifOperationsResponse {
668
+ return new ScanNotifOperationsResponse().fromJson(jsonValue, options)
669
+ }
670
+
671
+ static fromJsonString(
672
+ jsonString: string,
673
+ options?: Partial<JsonReadOptions>,
674
+ ): ScanNotifOperationsResponse {
675
+ return new ScanNotifOperationsResponse().fromJsonString(jsonString, options)
676
+ }
677
+
678
+ static equals(
679
+ a:
680
+ | ScanNotifOperationsResponse
681
+ | PlainMessage<ScanNotifOperationsResponse>
682
+ | undefined,
683
+ b:
684
+ | ScanNotifOperationsResponse
685
+ | PlainMessage<ScanNotifOperationsResponse>
686
+ | undefined,
687
+ ): boolean {
688
+ return proto3.util.equals(ScanNotifOperationsResponse, a, b)
689
+ }
690
+ }
691
+
375
692
  /**
376
693
  * Ping
377
694
  *
@@ -1,10 +1,5 @@
1
1
  import { sql } from 'kysely'
2
- import {
3
- AtUri,
4
- InvalidDidError,
5
- ensureValidAtUri,
6
- ensureValidDid,
7
- } from '@atproto/syntax'
2
+ import { AtUri } from '@atproto/syntax'
8
3
  import { Code, ConnectError, ServiceImpl } from '@connectrpc/connect'
9
4
  import { Service } from '../proto/bsync_connect'
10
5
  import { AddMuteOperationResponse, MuteOperation_Type } from '../proto/bsync_pb'
@@ -12,6 +7,7 @@ import AppContext from '../context'
12
7
  import { createMuteOpChannel } from '../db/schema/mute_op'
13
8
  import { authWithApiKey } from './auth'
14
9
  import Database from '../db'
10
+ import { isValidAtUri, isValidDid } from './util'
15
11
 
16
12
  export default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({
17
13
  async addMuteOperation(req, handlerCtx) {
@@ -120,9 +116,12 @@ const validMuteOp = (op: MuteOpInfo): MuteOpInfoValid => {
120
116
  // all good
121
117
  } else if (isValidAtUri(op.subject)) {
122
118
  const uri = new AtUri(op.subject)
123
- if (uri.collection !== 'app.bsky.graph.list') {
119
+ if (
120
+ uri.collection !== 'app.bsky.graph.list' &&
121
+ uri.collection !== 'app.bsky.feed.post'
122
+ ) {
124
123
  throw new ConnectError(
125
- 'subject aturis must reference a list record',
124
+ 'subject aturis must reference a list or post record',
126
125
  Code.InvalidArgument,
127
126
  )
128
127
  }
@@ -136,27 +135,6 @@ const validMuteOp = (op: MuteOpInfo): MuteOpInfoValid => {
136
135
  return op as MuteOpInfoValid // op.type has been checked
137
136
  }
138
137
 
139
- const isValidDid = (did: string) => {
140
- try {
141
- ensureValidDid(did)
142
- return true
143
- } catch (err) {
144
- if (err instanceof InvalidDidError) {
145
- return false
146
- }
147
- throw err
148
- }
149
- }
150
-
151
- const isValidAtUri = (uri: string) => {
152
- try {
153
- ensureValidAtUri(uri)
154
- return true
155
- } catch {
156
- return false
157
- }
158
- }
159
-
160
138
  type MuteOpInfo = {
161
139
  type: MuteOperation_Type
162
140
  actorDid: string
@@ -0,0 +1,80 @@
1
+ import { sql } from 'kysely'
2
+ import { Code, ConnectError, ServiceImpl } from '@connectrpc/connect'
3
+ import { Service } from '../proto/bsync_connect'
4
+ import { AddNotifOperationResponse } from '../proto/bsync_pb'
5
+ import AppContext from '../context'
6
+ import { authWithApiKey } from './auth'
7
+ import Database from '../db'
8
+ import { createNotifOpChannel } from '../db/schema/notif_op'
9
+ import { isValidDid } from './util'
10
+
11
+ export default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({
12
+ async addNotifOperation(req, handlerCtx) {
13
+ authWithApiKey(ctx, handlerCtx)
14
+ const { db } = ctx
15
+ const { actorDid, priority } = req
16
+ if (!isValidDid(actorDid)) {
17
+ throw new ConnectError(
18
+ 'actor_did must be a valid did',
19
+ Code.InvalidArgument,
20
+ )
21
+ }
22
+ const id = await db.transaction(async (txn) => {
23
+ // create notif op
24
+ const id = await createNotifOp(txn, actorDid, priority)
25
+ // update notif state
26
+ if (priority !== undefined) {
27
+ await updateNotifItem(txn, id, actorDid, priority)
28
+ }
29
+ return id
30
+ })
31
+ return new AddNotifOperationResponse({
32
+ operation: {
33
+ id: String(id),
34
+ actorDid,
35
+ priority,
36
+ },
37
+ })
38
+ },
39
+ })
40
+
41
+ const createNotifOp = async (
42
+ db: Database,
43
+ actorDid: string,
44
+ priority: boolean | undefined,
45
+ ) => {
46
+ const { ref } = db.db.dynamic
47
+ const { id } = await db.db
48
+ .insertInto('notif_op')
49
+ .values({
50
+ actorDid,
51
+ priority,
52
+ })
53
+ .returning('id')
54
+ .executeTakeFirstOrThrow()
55
+ await sql`notify ${ref(createNotifOpChannel)}`.execute(db.db) // emitted transactionally
56
+ return id
57
+ }
58
+
59
+ const updateNotifItem = async (
60
+ db: Database,
61
+ fromId: number,
62
+ actorDid: string,
63
+ priority: boolean,
64
+ ) => {
65
+ const { ref } = db.db.dynamic
66
+ await db.db
67
+ .insertInto('notif_item')
68
+ .values({
69
+ actorDid,
70
+ priority,
71
+ fromId,
72
+ })
73
+ .onConflict((oc) =>
74
+ oc.column('actorDid').doUpdateSet({
75
+ priority: sql`${ref('excluded.priority')}`,
76
+ fromId: sql`${ref('excluded.fromId')}`,
77
+ }),
78
+ )
79
+ .execute()
80
+ }
@@ -4,11 +4,15 @@ import { Service } from '../proto/bsync_connect'
4
4
  import AppContext from '../context'
5
5
  import addMuteOperation from './add-mute-operation'
6
6
  import scanMuteOperations from './scan-mute-operations'
7
+ import addNotifOperation from './add-notif-operation'
8
+ import scanNotifOperations from './scan-notif-operations'
7
9
 
8
10
  export default (ctx: AppContext) => (router: ConnectRouter) => {
9
11
  return router.service(Service, {
10
12
  ...addMuteOperation(ctx),
11
13
  ...scanMuteOperations(ctx),
14
+ ...addNotifOperation(ctx),
15
+ ...scanNotifOperations(ctx),
12
16
  async ping() {
13
17
  const { db } = ctx
14
18
  await sql`select 1`.execute(db.db)
@@ -1,10 +1,11 @@
1
1
  import { once } from 'node:events'
2
- import { Code, ConnectError, ServiceImpl } from '@connectrpc/connect'
2
+ import { ServiceImpl } from '@connectrpc/connect'
3
3
  import { Service } from '../proto/bsync_connect'
4
4
  import { ScanMuteOperationsResponse } from '../proto/bsync_pb'
5
5
  import AppContext from '../context'
6
6
  import { createMuteOpChannel } from '../db/schema/mute_op'
7
7
  import { authWithApiKey } from './auth'
8
+ import { combineSignals, validCursor } from './util'
8
9
 
9
10
  export default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({
10
11
  async scanMuteOperations(req, handlerCtx) {
@@ -62,27 +63,3 @@ export default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({
62
63
  })
63
64
  },
64
65
  })
65
-
66
- const validCursor = (cursor: string): number | null => {
67
- if (cursor === '') return null
68
- const int = parseInt(cursor, 10)
69
- if (isNaN(int) || int < 0) {
70
- throw new ConnectError('invalid cursor', Code.InvalidArgument)
71
- }
72
- return int
73
- }
74
-
75
- const combineSignals = (a: AbortSignal, b: AbortSignal) => {
76
- const controller = new AbortController()
77
- for (const signal of [a, b]) {
78
- if (signal.aborted) {
79
- controller.abort()
80
- return signal
81
- }
82
- signal.addEventListener('abort', () => controller.abort(signal.reason), {
83
- // @ts-ignore https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68625
84
- signal: controller.signal,
85
- })
86
- }
87
- return controller.signal
88
- }
@@ -0,0 +1,64 @@
1
+ import { once } from 'node:events'
2
+ import { ServiceImpl } from '@connectrpc/connect'
3
+ import { Service } from '../proto/bsync_connect'
4
+ import { ScanNotifOperationsResponse } from '../proto/bsync_pb'
5
+ import AppContext from '../context'
6
+ import { authWithApiKey } from './auth'
7
+ import { combineSignals, validCursor } from './util'
8
+ import { createNotifOpChannel } from '../db/schema/notif_op'
9
+
10
+ export default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({
11
+ async scanNotifOperations(req, handlerCtx) {
12
+ authWithApiKey(ctx, handlerCtx)
13
+ const { db, events } = ctx
14
+ const limit = req.limit || 1000
15
+ const cursor = validCursor(req.cursor)
16
+ const nextNotifOpPromise = once(events, createNotifOpChannel, {
17
+ signal: combineSignals(
18
+ ctx.shutdown,
19
+ AbortSignal.timeout(ctx.cfg.service.longPollTimeoutMs),
20
+ ),
21
+ })
22
+ nextNotifOpPromise.catch(() => null) // ensure timeout is always handled
23
+
24
+ const nextNotifOpPageQb = db.db
25
+ .selectFrom('notif_op')
26
+ .selectAll()
27
+ .where('id', '>', cursor ?? -1)
28
+ .orderBy('id', 'asc')
29
+ .limit(limit)
30
+
31
+ let ops = await nextNotifOpPageQb.execute()
32
+
33
+ if (!ops.length) {
34
+ // if there were no ops on the page, wait for an event then try again.
35
+ try {
36
+ await nextNotifOpPromise
37
+ } catch (err) {
38
+ ctx.shutdown.throwIfAborted()
39
+ return new ScanNotifOperationsResponse({
40
+ operations: [],
41
+ cursor: req.cursor,
42
+ })
43
+ }
44
+ ops = await nextNotifOpPageQb.execute()
45
+ if (!ops.length) {
46
+ return new ScanNotifOperationsResponse({
47
+ operations: [],
48
+ cursor: req.cursor,
49
+ })
50
+ }
51
+ }
52
+
53
+ const lastOp = ops[ops.length - 1]
54
+
55
+ return new ScanNotifOperationsResponse({
56
+ operations: ops.map((op) => ({
57
+ id: op.id.toString(),
58
+ actorDid: op.actorDid,
59
+ priority: op.priority ?? undefined,
60
+ })),
61
+ cursor: lastOp.id.toString(),
62
+ })
63
+ },
64
+ })
@@ -0,0 +1,51 @@
1
+ import { Code, ConnectError } from '@connectrpc/connect'
2
+ import {
3
+ InvalidDidError,
4
+ ensureValidAtUri,
5
+ ensureValidDid,
6
+ } from '@atproto/syntax'
7
+
8
+ export const validCursor = (cursor: string): number | null => {
9
+ if (cursor === '') return null
10
+ const int = parseInt(cursor, 10)
11
+ if (isNaN(int) || int < 0) {
12
+ throw new ConnectError('invalid cursor', Code.InvalidArgument)
13
+ }
14
+ return int
15
+ }
16
+
17
+ export const combineSignals = (a: AbortSignal, b: AbortSignal) => {
18
+ const controller = new AbortController()
19
+ for (const signal of [a, b]) {
20
+ if (signal.aborted) {
21
+ controller.abort()
22
+ return signal
23
+ }
24
+ signal.addEventListener('abort', () => controller.abort(signal.reason), {
25
+ // @ts-ignore https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68625
26
+ signal: controller.signal,
27
+ })
28
+ }
29
+ return controller.signal
30
+ }
31
+
32
+ export const isValidDid = (did: string) => {
33
+ try {
34
+ ensureValidDid(did)
35
+ return true
36
+ } catch (err) {
37
+ if (err instanceof InvalidDidError) {
38
+ return false
39
+ }
40
+ throw err
41
+ }
42
+ }
43
+
44
+ export const isValidAtUri = (uri: string) => {
45
+ try {
46
+ ensureValidAtUri(uri)
47
+ return true
48
+ } catch {
49
+ return false
50
+ }
51
+ }
@@ -1,4 +1,5 @@
1
1
  import { wait } from '@atproto/common'
2
+ import getPort from 'get-port'
2
3
  import { Code, ConnectError } from '@connectrpc/connect'
3
4
  import {
4
5
  BsyncClient,
@@ -17,6 +18,7 @@ describe('mutes', () => {
17
18
  beforeAll(async () => {
18
19
  bsync = await BsyncService.create(
19
20
  envToCfg({
21
+ port: await getPort(),
20
22
  dbUrl: process.env.DB_POSTGRES_URL,
21
23
  dbSchema: 'bsync_mutes',
22
24
  apiKeys: ['key-1'],