@atcute/tap 0.1.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.
@@ -0,0 +1,56 @@
1
+ import type { Did, Handle, Nsid, RecordKey, Tid } from '@atcute/lexicons/syntax';
2
+ import type { CloseEvent, ErrorEvent, Options } from 'partysocket/ws';
3
+ export type TapRecordAction = 'create' | 'update' | 'delete';
4
+ export type TapRepoStatus = 'active' | 'takendown' | 'suspended' | 'deactivated' | 'deleted';
5
+ export interface TapRecordEvent {
6
+ id: number;
7
+ type: 'record';
8
+ live: boolean;
9
+ did: Did;
10
+ rev: Tid;
11
+ collection: Nsid;
12
+ rkey: RecordKey;
13
+ action: TapRecordAction;
14
+ record?: Record<string, unknown>;
15
+ cid?: string;
16
+ }
17
+ export interface TapIdentityEvent {
18
+ id: number;
19
+ type: 'identity';
20
+ did: Did;
21
+ handle: Handle;
22
+ isActive: boolean;
23
+ status: TapRepoStatus;
24
+ }
25
+ export type TapEvent = TapRecordEvent | TapIdentityEvent;
26
+ export interface RepoInfo {
27
+ did: Did;
28
+ handle: Handle;
29
+ state: string;
30
+ rev: Tid;
31
+ records: number;
32
+ error?: string;
33
+ retries?: number;
34
+ }
35
+ export interface TapClientOptions {
36
+ url: string | URL;
37
+ adminPassword?: string;
38
+ fetch?: typeof globalThis.fetch;
39
+ }
40
+ export interface TapSubscribeOptions {
41
+ /**
42
+ * whether to validate incoming events.
43
+ * @default true
44
+ */
45
+ validateEvents?: boolean;
46
+ onConnectionOpen?: (event: Event) => void;
47
+ onConnectionClose?: (event: CloseEvent) => void;
48
+ onConnectionError?: (event: ErrorEvent) => void;
49
+ onError?: (error: unknown) => void;
50
+ ws?: Options;
51
+ }
52
+ export interface TapSubscriptionMessage {
53
+ event: TapEvent;
54
+ ack: () => Promise<void>;
55
+ }
56
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../lib/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,yBAAyB,CAAC;AACjF,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEtE,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE7D,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,WAAW,GAAG,WAAW,GAAG,aAAa,GAAG,SAAS,CAAC;AAE7F,MAAM,WAAW,cAAc;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,QAAQ,CAAC;IAEf,IAAI,EAAE,OAAO,CAAC;IACd,GAAG,EAAE,GAAG,CAAC;IACT,GAAG,EAAE,GAAG,CAAC;IACT,UAAU,EAAE,IAAI,CAAC;IACjB,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,UAAU,CAAC;IAEjB,GAAG,EAAE,GAAG,CAAC;IACT,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,aAAa,CAAC;CACtB;AAED,MAAM,MAAM,QAAQ,GAAG,cAAc,GAAG,gBAAgB,CAAC;AAEzD,MAAM,WAAW,QAAQ;IACxB,GAAG,EAAE,GAAG,CAAC;IACT,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAChC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CAChC;AAED,MAAM,WAAW,mBAAmB;IACnC;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAC1C,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAChD,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAChD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAEnC,EAAE,CAAC,EAAE,OAAO,CAAC;CACb;AAED,MAAM,WAAW,sBAAsB;IACtC,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../lib/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export declare const formatAdminAuthHeader: (password: string) => string;
2
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../lib/utils.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,qBAAqB,8BAEjC,CAAC"}
package/dist/utils.js ADDED
@@ -0,0 +1,6 @@
1
+ import { toBase64Pad } from '@atcute/multibase';
2
+ import { encodeUtf8 } from '@atcute/uint8array';
3
+ export const formatAdminAuthHeader = (password) => {
4
+ return `Basic ${toBase64Pad(encodeUtf8(`admin:${password}`))}`;
5
+ };
6
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,QAAgB,EAAU,EAAE,CAAC;IAClE,OAAO,SAAS,WAAW,CAAC,UAAU,CAAC,SAAS,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC;AAAA,CAC/D,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { TapClient } from './tap-client.js';
2
+
3
+ const tap = new TapClient({ url: 'http://localhost:2480' });
4
+
5
+ const subscription = tap.subscribe({
6
+ onConnectionOpen() {
7
+ console.log(`ws open`);
8
+ },
9
+ onConnectionClose() {
10
+ console.log(`ws close`);
11
+ },
12
+ onConnectionError(ev) {
13
+ console.log(`ws error`, ev);
14
+ },
15
+ onError(err) {
16
+ console.error('tap subscription error', err);
17
+ },
18
+ });
19
+
20
+ for await (const { event, ack } of subscription) {
21
+ console.log(event);
22
+
23
+ await ack();
24
+ }
package/lib/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { TapClient } from './tap-client.js';
2
+ export { TapSubscription } from './tap-subscription.js';
3
+
4
+ export * as defs from './typedefs.js';
5
+ export type * from './types.js';
@@ -0,0 +1,114 @@
1
+ import { defs as identityDefs, type DidDocument } from '@atcute/identity';
2
+
3
+ import type { Did } from '@atcute/lexicons';
4
+
5
+ import { TapSubscription } from './tap-subscription.js';
6
+ import { repoInfoSchema } from './typedefs.js';
7
+ import type { RepoInfo, TapClientOptions, TapSubscribeOptions } from './types.js';
8
+ import { formatAdminAuthHeader } from './utils.js';
9
+
10
+ export class TapClient {
11
+ #url: URL;
12
+ #fetch: typeof globalThis.fetch;
13
+ #adminPassword?: string;
14
+ #authHeader?: string;
15
+
16
+ constructor(options: TapClientOptions) {
17
+ const url = typeof options.url === 'string' ? new URL(options.url) : new URL(options.url);
18
+
19
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
20
+ throw new Error(`invalid url protocol, expected http: or https:, got ${url.protocol}`);
21
+ }
22
+
23
+ this.#url = url;
24
+ this.#fetch = options.fetch ?? fetch;
25
+
26
+ if (options.adminPassword) {
27
+ this.#adminPassword = options.adminPassword;
28
+ this.#authHeader = formatAdminAuthHeader(options.adminPassword);
29
+ }
30
+ }
31
+
32
+ subscribe(options?: TapSubscribeOptions): TapSubscription {
33
+ const wsUrl = new URL(this.#url);
34
+ wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:';
35
+ wsUrl.pathname = '/channel';
36
+
37
+ return new TapSubscription({
38
+ url: wsUrl.toString(),
39
+ adminPassword: this.#adminPassword,
40
+ ...options,
41
+ });
42
+ }
43
+
44
+ async addRepos(dids: Did[]): Promise<void> {
45
+ const response = await this.#fetch(new URL('/repos/add', this.#url), {
46
+ method: 'POST',
47
+ headers: this.#getHeaders(),
48
+ body: JSON.stringify({ dids }),
49
+ });
50
+
51
+ await response.body?.cancel();
52
+ if (!response.ok) {
53
+ throw new Error(`failed to add repos: ${response.status} ${response.statusText}`);
54
+ }
55
+ }
56
+
57
+ async removeRepos(dids: Did[]): Promise<void> {
58
+ const response = await this.#fetch(new URL('/repos/remove', this.#url), {
59
+ method: 'POST',
60
+ headers: this.#getHeaders(),
61
+ body: JSON.stringify({ dids }),
62
+ });
63
+
64
+ await response.body?.cancel();
65
+ if (!response.ok) {
66
+ throw new Error(`failed to remove repos: ${response.status} ${response.statusText}`);
67
+ }
68
+ }
69
+
70
+ async resolveDid(did: Did): Promise<DidDocument | null> {
71
+ const response = await this.#fetch(new URL(`/resolve/${did}`, this.#url), {
72
+ method: 'GET',
73
+ headers: this.#getHeaders(),
74
+ });
75
+
76
+ if (response.status === 404) {
77
+ await response.body?.cancel();
78
+ return null;
79
+ }
80
+
81
+ if (!response.ok) {
82
+ await response.body?.cancel();
83
+ throw new Error(`failed to resolve did: ${response.status} ${response.statusText}`);
84
+ }
85
+
86
+ return identityDefs.didDocument.parse(await response.json());
87
+ }
88
+
89
+ async getRepoInfo(did: Did): Promise<RepoInfo> {
90
+ const response = await this.#fetch(new URL(`/info/${did}`, this.#url), {
91
+ method: 'GET',
92
+ headers: this.#getHeaders(),
93
+ });
94
+
95
+ if (!response.ok) {
96
+ await response.body?.cancel();
97
+ throw new Error(`failed to get repo info: ${response.status} ${response.statusText}`);
98
+ }
99
+
100
+ return repoInfoSchema.parse(await response.json());
101
+ }
102
+
103
+ #getHeaders(): Record<string, string> {
104
+ const headers: Record<string, string> = {
105
+ 'Content-Type': 'application/json',
106
+ };
107
+
108
+ if (this.#authHeader) {
109
+ headers['Authorization'] = this.#authHeader;
110
+ }
111
+
112
+ return headers;
113
+ }
114
+ }
@@ -0,0 +1,267 @@
1
+ import { EventIterator } from '@mary-ext/event-iterator';
2
+ import { SimpleEventEmitter } from '@mary-ext/simple-event-emitter';
3
+ import { WebSocket as ReconnectingWebSocket } from 'partysocket';
4
+
5
+ import type { ReadonlyDeep } from 'type-fest';
6
+
7
+ import { decodeUtf8From } from '@atcute/uint8array';
8
+
9
+ import { flattenTapEvent, tapEventWireSchema } from './typedefs.js';
10
+ import type { TapEvent, TapSubscribeOptions, TapSubscriptionMessage } from './types.js';
11
+ import { formatAdminAuthHeader } from './utils.js';
12
+
13
+ export interface TapSubscriptionOptions extends TapSubscribeOptions {
14
+ url: string;
15
+ adminPassword?: string;
16
+ }
17
+
18
+ type BufferedAck = {
19
+ id: number;
20
+ promise: Promise<void>;
21
+ resolve: (value: void) => void;
22
+ reject: (reason?: unknown) => void;
23
+ };
24
+
25
+ const PARSE_OPTIONS = { mode: 'passthrough' } as const;
26
+
27
+ export class TapSubscription {
28
+ #listening = 0;
29
+ #ws?: ReconnectingWebSocket;
30
+
31
+ #emitter = new SimpleEventEmitter<[message: TapSubscriptionMessage]>();
32
+ #bufferedAcks: BufferedAck[] = [];
33
+
34
+ #options: TapSubscriptionOptions;
35
+ #closed = false;
36
+
37
+ constructor(options: TapSubscriptionOptions) {
38
+ this.#options = options;
39
+ }
40
+
41
+ #sendAck(id: number): boolean {
42
+ const ws = this.#ws;
43
+ if (ws === undefined) {
44
+ return false;
45
+ }
46
+
47
+ if (ws.readyState !== 1) {
48
+ return false;
49
+ }
50
+
51
+ ws.send(JSON.stringify({ type: 'ack', id }));
52
+ return true;
53
+ }
54
+
55
+ async #ackEvent(id: number): Promise<void> {
56
+ if (this.#closed) {
57
+ throw new Error(`tap subscription is closed`);
58
+ }
59
+
60
+ try {
61
+ if (this.#sendAck(id)) {
62
+ return;
63
+ }
64
+ } catch {
65
+ // fall through to buffering
66
+ }
67
+
68
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
69
+ this.#bufferedAcks.push({ id, promise, resolve, reject });
70
+ return await promise;
71
+ }
72
+
73
+ #flushBufferedAcks() {
74
+ while (this.#bufferedAcks.length > 0) {
75
+ const ack = this.#bufferedAcks[0];
76
+ if (ack === undefined) {
77
+ return;
78
+ }
79
+
80
+ try {
81
+ if (!this.#sendAck(ack.id)) {
82
+ return;
83
+ }
84
+
85
+ ack.resolve(undefined);
86
+ this.#bufferedAcks = this.#bufferedAcks.slice(1);
87
+ } catch (err) {
88
+ this.#options.onError?.(err);
89
+ return;
90
+ }
91
+ }
92
+ }
93
+
94
+ #create() {
95
+ if (this.#ws !== undefined) {
96
+ return;
97
+ }
98
+
99
+ const {
100
+ url,
101
+ adminPassword,
102
+ ws: wsOptions,
103
+ validateEvents = true,
104
+ onConnectionClose,
105
+ onConnectionError,
106
+ onConnectionOpen,
107
+ onError,
108
+ } = this.#options;
109
+
110
+ const emitter = this.#emitter;
111
+
112
+ const authHeader = adminPassword ? formatAdminAuthHeader(adminPassword) : undefined;
113
+
114
+ const mergedWsOptions =
115
+ authHeader !== undefined && wsOptions?.WebSocket === undefined
116
+ ? {
117
+ ...wsOptions,
118
+ WebSocket: createAuthedWebSocket(authHeader),
119
+ }
120
+ : wsOptions;
121
+
122
+ const ws = new ReconnectingWebSocket(() => url, null, mergedWsOptions);
123
+ this.#ws = ws;
124
+
125
+ ws.binaryType = 'arraybuffer';
126
+
127
+ ws.onclose = onConnectionClose ?? null;
128
+ ws.onerror = onConnectionError ?? null;
129
+
130
+ ws.onopen = (ev) => {
131
+ this.#flushBufferedAcks();
132
+ onConnectionOpen?.(ev);
133
+ };
134
+
135
+ ws.onmessage = (ev) => {
136
+ let raw: unknown;
137
+ try {
138
+ const data = toMessageText(ev.data);
139
+ raw = JSON.parse(data);
140
+ } catch (err) {
141
+ onError?.(new Error(`failed to parse tap message`, { cause: err }));
142
+ return;
143
+ }
144
+
145
+ let evt: TapEvent;
146
+ if (validateEvents) {
147
+ const result = tapEventWireSchema.try(raw, PARSE_OPTIONS);
148
+ if (!result.ok) {
149
+ onError?.(result);
150
+ return;
151
+ }
152
+
153
+ evt = flattenTapEvent(result.value);
154
+ } else {
155
+ try {
156
+ evt = flattenTapEvent(raw as any);
157
+ } catch (err) {
158
+ onError?.(err);
159
+ return;
160
+ }
161
+ }
162
+
163
+ let acked = false;
164
+ let ackPromise: Promise<void> | undefined;
165
+
166
+ emitter.emit({
167
+ event: evt,
168
+ ack: () => {
169
+ if (!acked) {
170
+ acked = true;
171
+ ackPromise = this.#ackEvent(evt.id);
172
+ }
173
+ return ackPromise!;
174
+ },
175
+ });
176
+ };
177
+ }
178
+
179
+ #destroy() {
180
+ const ws = this.#ws;
181
+ if (ws) {
182
+ ws.close();
183
+ this.#ws = undefined;
184
+ }
185
+
186
+ this.#closed = true;
187
+
188
+ if (this.#bufferedAcks.length > 0) {
189
+ const err = new Error(`tap subscription closed before ack was sent`);
190
+ for (const ack of this.#bufferedAcks) {
191
+ ack.reject(err);
192
+ }
193
+ this.#bufferedAcks = [];
194
+ }
195
+ }
196
+
197
+ [Symbol.asyncIterator]() {
198
+ return new EventIterator<TapSubscriptionMessage>((emit) => {
199
+ if (this.#listening === 0) {
200
+ this.#closed = false;
201
+ this.#create();
202
+ }
203
+
204
+ this.#listening++;
205
+ this.#emitter.subscribe(emit);
206
+
207
+ return () => {
208
+ if (this.#listening === 1) {
209
+ this.#destroy();
210
+ }
211
+
212
+ this.#listening--;
213
+ this.#emitter.unsubscribe(emit);
214
+ };
215
+ });
216
+ }
217
+
218
+ getOptions(): ReadonlyDeep<TapSubscriptionOptions> {
219
+ return this.#options;
220
+ }
221
+
222
+ updateOptions(options: Partial<TapSubscriptionOptions>): void {
223
+ this.#options = { ...this.#options, ...options };
224
+
225
+ if (this.#ws !== undefined) {
226
+ this.#destroy();
227
+ this.#closed = false;
228
+ this.#create();
229
+ }
230
+ }
231
+ }
232
+
233
+ const toMessageText = (data: unknown): string => {
234
+ if (typeof data === 'string') {
235
+ return data;
236
+ }
237
+
238
+ if (data instanceof ArrayBuffer) {
239
+ return decodeUtf8From(new Uint8Array(data));
240
+ }
241
+
242
+ if (ArrayBuffer.isView(data)) {
243
+ return decodeUtf8From(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
244
+ }
245
+
246
+ return String(data);
247
+ };
248
+
249
+ const createAuthedWebSocket = (authorization: string) => {
250
+ const WebSocketCtor = WebSocket as unknown as {
251
+ new (
252
+ url: string | URL,
253
+ protocols?: string | string[],
254
+ options?: { headers?: Record<string, string> },
255
+ ): WebSocket;
256
+ };
257
+
258
+ return class AuthedWebSocket extends WebSocketCtor {
259
+ constructor(url: string | URL, protocols?: string | string[]) {
260
+ super(url, protocols as any, {
261
+ headers: {
262
+ Authorization: authorization,
263
+ },
264
+ });
265
+ }
266
+ };
267
+ };
@@ -0,0 +1,102 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ import { isDid, isHandle, isNsid, isRecordKey, isTid } from '@atcute/lexicons/syntax';
4
+
5
+ import type * as t from './types.js';
6
+
7
+ const didString = v.string().assert(isDid, `must be a did`);
8
+ const handleString = v.string().assert(isHandle, `must be a handle`);
9
+ const nsidString = v.string().assert(isNsid, `must be an nsid`);
10
+ const rkeyString = v.string().assert(isRecordKey, `must be a record key`);
11
+ const tidString = v.string().assert(isTid, `must be a tid`);
12
+
13
+ const integer = v
14
+ .number()
15
+ .assert((input) => input >= 0 && Number.isSafeInteger(input), `must be a nonnegative integer`);
16
+
17
+ const recordEventDataSchema = v.object({
18
+ did: didString,
19
+ rev: tidString,
20
+ collection: nsidString,
21
+ rkey: rkeyString,
22
+ action: v.union(v.literal('create'), v.literal('update'), v.literal('delete')),
23
+ record: v.record(v.unknown()).optional(),
24
+ cid: v.string().optional(),
25
+ live: v.boolean(),
26
+ });
27
+
28
+ const identityEventDataSchema = v.object({
29
+ did: didString,
30
+ handle: handleString,
31
+ is_active: v.boolean(),
32
+ status: v.union(
33
+ v.literal('active'),
34
+ v.literal('takendown'),
35
+ v.literal('suspended'),
36
+ v.literal('deactivated'),
37
+ v.literal('deleted'),
38
+ ),
39
+ });
40
+
41
+ export const tapRecordEventWireSchema = v.object({
42
+ id: integer,
43
+ type: v.literal('record'),
44
+ record: recordEventDataSchema,
45
+ });
46
+
47
+ export const tapIdentityEventWireSchema = v.object({
48
+ id: integer,
49
+ type: v.literal('identity'),
50
+ identity: identityEventDataSchema,
51
+ });
52
+
53
+ export const tapEventWireSchema = v.union(tapRecordEventWireSchema, tapIdentityEventWireSchema);
54
+
55
+ export const repoInfoSchema: v.Type<t.RepoInfo> = v.object({
56
+ did: didString,
57
+ handle: handleString,
58
+ state: v.string(),
59
+ rev: tidString,
60
+ records: integer,
61
+ error: v.string().optional(),
62
+ retries: integer.optional(),
63
+ });
64
+
65
+ export const flattenTapEvent = (wire: v.Infer<typeof tapEventWireSchema>): t.TapEvent => {
66
+ switch (wire.type) {
67
+ case 'identity': {
68
+ return {
69
+ id: wire.id,
70
+ type: 'identity',
71
+
72
+ did: wire.identity.did,
73
+ handle: wire.identity.handle,
74
+ isActive: wire.identity.is_active,
75
+ status: wire.identity.status,
76
+ };
77
+ }
78
+
79
+ case 'record': {
80
+ return {
81
+ id: wire.id,
82
+ type: 'record',
83
+ live: wire.record.live,
84
+
85
+ rev: wire.record.rev,
86
+ did: wire.record.did,
87
+ collection: wire.record.collection,
88
+ rkey: wire.record.rkey,
89
+ cid: wire.record.cid,
90
+ action: wire.record.action,
91
+ record: wire.record.record,
92
+ };
93
+ }
94
+
95
+ default: {
96
+ wire satisfies never;
97
+
98
+ const obj = wire as any;
99
+ throw new Error(`unknown "${obj.type}" type`);
100
+ }
101
+ }
102
+ };
package/lib/types.ts ADDED
@@ -0,0 +1,68 @@
1
+ import type { Did, Handle, Nsid, RecordKey, Tid } from '@atcute/lexicons/syntax';
2
+ import type { CloseEvent, ErrorEvent, Options } from 'partysocket/ws';
3
+
4
+ export type TapRecordAction = 'create' | 'update' | 'delete';
5
+
6
+ export type TapRepoStatus = 'active' | 'takendown' | 'suspended' | 'deactivated' | 'deleted';
7
+
8
+ export interface TapRecordEvent {
9
+ id: number;
10
+ type: 'record';
11
+
12
+ live: boolean;
13
+ did: Did;
14
+ rev: Tid;
15
+ collection: Nsid;
16
+ rkey: RecordKey;
17
+ action: TapRecordAction;
18
+ record?: Record<string, unknown>;
19
+ cid?: string;
20
+ }
21
+
22
+ export interface TapIdentityEvent {
23
+ id: number;
24
+ type: 'identity';
25
+
26
+ did: Did;
27
+ handle: Handle;
28
+ isActive: boolean;
29
+ status: TapRepoStatus;
30
+ }
31
+
32
+ export type TapEvent = TapRecordEvent | TapIdentityEvent;
33
+
34
+ export interface RepoInfo {
35
+ did: Did;
36
+ handle: Handle;
37
+ state: string;
38
+ rev: Tid;
39
+ records: number;
40
+ error?: string;
41
+ retries?: number;
42
+ }
43
+
44
+ export interface TapClientOptions {
45
+ url: string | URL;
46
+ adminPassword?: string;
47
+ fetch?: typeof globalThis.fetch;
48
+ }
49
+
50
+ export interface TapSubscribeOptions {
51
+ /**
52
+ * whether to validate incoming events.
53
+ * @default true
54
+ */
55
+ validateEvents?: boolean;
56
+
57
+ onConnectionOpen?: (event: Event) => void;
58
+ onConnectionClose?: (event: CloseEvent) => void;
59
+ onConnectionError?: (event: ErrorEvent) => void;
60
+ onError?: (error: unknown) => void;
61
+
62
+ ws?: Options;
63
+ }
64
+
65
+ export interface TapSubscriptionMessage {
66
+ event: TapEvent;
67
+ ack: () => Promise<void>;
68
+ }
package/lib/utils.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { toBase64Pad } from '@atcute/multibase';
2
+ import { encodeUtf8 } from '@atcute/uint8array';
3
+
4
+ export const formatAdminAuthHeader = (password: string): string => {
5
+ return `Basic ${toBase64Pad(encodeUtf8(`admin:${password}`))}`;
6
+ };