document-drive 1.0.0-websockets → 1.0.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 (43) hide show
  1. package/README.md +1 -0
  2. package/package.json +74 -88
  3. package/src/cache/index.ts +2 -2
  4. package/src/cache/memory.ts +22 -13
  5. package/src/cache/redis.ts +43 -16
  6. package/src/cache/types.ts +4 -4
  7. package/src/index.ts +6 -3
  8. package/src/queue/base.ts +276 -214
  9. package/src/queue/index.ts +2 -2
  10. package/src/queue/redis.ts +138 -127
  11. package/src/queue/types.ts +44 -38
  12. package/src/read-mode/errors.ts +19 -0
  13. package/src/read-mode/index.ts +125 -0
  14. package/src/read-mode/service.ts +207 -0
  15. package/src/read-mode/types.ts +108 -0
  16. package/src/server/error.ts +61 -26
  17. package/src/server/index.ts +2160 -1785
  18. package/src/server/listener/index.ts +2 -2
  19. package/src/server/listener/manager.ts +475 -437
  20. package/src/server/listener/transmitter/index.ts +4 -5
  21. package/src/server/listener/transmitter/internal.ts +77 -79
  22. package/src/server/listener/transmitter/pull-responder.ts +363 -329
  23. package/src/server/listener/transmitter/switchboard-push.ts +72 -55
  24. package/src/server/listener/transmitter/types.ts +19 -25
  25. package/src/server/types.ts +536 -349
  26. package/src/server/utils.ts +26 -27
  27. package/src/storage/base.ts +81 -0
  28. package/src/storage/browser.ts +233 -216
  29. package/src/storage/filesystem.ts +257 -256
  30. package/src/storage/index.ts +2 -1
  31. package/src/storage/memory.ts +206 -214
  32. package/src/storage/prisma.ts +575 -568
  33. package/src/storage/sequelize.ts +460 -471
  34. package/src/storage/types.ts +83 -67
  35. package/src/utils/default-drives-manager.ts +341 -0
  36. package/src/utils/document-helpers.ts +19 -18
  37. package/src/utils/graphql.ts +288 -34
  38. package/src/utils/index.ts +61 -59
  39. package/src/utils/logger.ts +39 -37
  40. package/src/utils/migrations.ts +58 -0
  41. package/src/utils/run-asap.ts +156 -0
  42. package/CHANGELOG.md +0 -818
  43. package/src/server/listener/transmitter/subscription.ts +0 -364
@@ -1,375 +1,409 @@
1
- import { ListenerFilter, Trigger } from 'document-model-libs/document-drive';
2
- import { OperationScope } from 'document-model/document';
3
- import { PULL_DRIVE_INTERVAL } from '../..';
4
- import { generateUUID } from '../../../utils';
5
- import { gql, requestGraphql } from '../../../utils/graphql';
6
- import { logger as defaultLogger } from '../../../utils/logger';
7
- import { OperationError } from '../../error';
1
+ import { ListenerFilter, Trigger } from "document-model-libs/document-drive";
2
+ import { Operation, OperationScope } from "document-model/document";
3
+ import { PULL_DRIVE_INTERVAL } from "../..";
4
+ import { generateUUID } from "../../../utils";
5
+ import { gql, requestGraphql } from "../../../utils/graphql";
6
+ import { logger as defaultLogger } from "../../../utils/logger";
7
+ import { OperationError } from "../../error";
8
8
  import {
9
- BaseDocumentDriveServer,
10
- IOperationResult,
11
- Listener,
12
- ListenerRevision,
13
- ListenerRevisionWithError,
14
- OperationUpdate,
15
- RemoteDriveOptions,
16
- StrandUpdate
17
- } from '../../types';
18
- import { ListenerManager } from '../manager';
19
- import { ITriggerTransmitter, PullResponderTrigger } from './types';
20
-
21
- export type OperationUpdateGraphQL = Omit<OperationUpdate, 'input'> & {
22
- input: string;
9
+ GetStrandsOptions,
10
+ IBaseDocumentDriveServer,
11
+ IOperationResult,
12
+ Listener,
13
+ ListenerRevision,
14
+ ListenerRevisionWithError,
15
+ OperationUpdate,
16
+ RemoteDriveOptions,
17
+ StrandUpdate,
18
+ } from "../../types";
19
+ import { ListenerManager } from "../manager";
20
+ import {
21
+ ITransmitter,
22
+ PullResponderTrigger,
23
+ StrandUpdateSource,
24
+ } from "./types";
25
+
26
+ export type OperationUpdateGraphQL = Omit<OperationUpdate, "input"> & {
27
+ input: string;
23
28
  };
24
29
 
25
30
  export type PullStrandsGraphQL = {
26
- system: {
27
- sync: {
28
- strands: StrandUpdateGraphQL[];
29
- };
31
+ system: {
32
+ sync: {
33
+ strands: StrandUpdateGraphQL[];
30
34
  };
35
+ };
31
36
  };
32
37
 
33
38
  export type CancelPullLoop = () => void;
34
39
 
35
- export type StrandUpdateGraphQL = Omit<StrandUpdate, 'operations'> & {
36
- operations: OperationUpdateGraphQL[];
40
+ export type StrandUpdateGraphQL = Omit<StrandUpdate, "operations"> & {
41
+ operations: OperationUpdateGraphQL[];
37
42
  };
38
43
 
39
- export interface IPullResponderTransmitter extends ITriggerTransmitter {
40
- getStrands(since?: string): Promise<StrandUpdate[]>;
44
+ export interface IPullResponderTransmitter extends ITransmitter {
45
+ getStrands(options?: GetStrandsOptions): Promise<StrandUpdate[]>;
41
46
  }
42
47
 
43
48
  export class PullResponderTransmitter implements IPullResponderTransmitter {
44
- private drive: BaseDocumentDriveServer;
45
- private listener: Listener;
46
- private manager: ListenerManager;
47
-
48
- constructor(
49
- listener: Listener,
50
- drive: BaseDocumentDriveServer,
51
- manager: ListenerManager
52
- ) {
53
- this.listener = listener;
54
- this.drive = drive;
55
- this.manager = manager;
56
- }
49
+ private drive: IBaseDocumentDriveServer;
50
+ private listener: Listener;
51
+ private manager: ListenerManager;
57
52
 
58
- async transmit(): Promise<ListenerRevision[]> {
59
- return [];
60
- }
53
+ constructor(
54
+ listener: Listener,
55
+ drive: IBaseDocumentDriveServer,
56
+ manager: ListenerManager,
57
+ ) {
58
+ this.listener = listener;
59
+ this.drive = drive;
60
+ this.manager = manager;
61
+ }
61
62
 
62
- getStrands(since?: string | undefined): Promise<StrandUpdate[]> {
63
- return this.manager.getStrands(
64
- this.listener.driveId,
65
- this.listener.listenerId,
66
- since
67
- );
63
+ getStrands(options?: GetStrandsOptions): Promise<StrandUpdate[]> {
64
+ return this.manager.getStrands(
65
+ this.listener.driveId,
66
+ this.listener.listenerId,
67
+ options,
68
+ );
69
+ }
70
+
71
+ disconnect(): Promise<void> {
72
+ // TODO remove listener from switchboard
73
+ return Promise.resolve();
74
+ }
75
+
76
+ async processAcknowledge(
77
+ driveId: string,
78
+ listenerId: string,
79
+ revisions: ListenerRevision[],
80
+ ): Promise<boolean> {
81
+ const syncUnits = await this.manager.getListenerSyncUnitIds(
82
+ driveId,
83
+ listenerId,
84
+ );
85
+ let success = true;
86
+ for (const revision of revisions) {
87
+ const syncUnit = syncUnits.find(
88
+ (s) =>
89
+ s.scope === revision.scope &&
90
+ s.branch === revision.branch &&
91
+ s.driveId === revision.driveId &&
92
+ s.documentId == revision.documentId,
93
+ );
94
+ if (!syncUnit) {
95
+ defaultLogger.warn("Unknown sync unit was acknowledged", revision);
96
+ success = false;
97
+ continue;
98
+ }
99
+
100
+ await this.manager.updateListenerRevision(
101
+ listenerId,
102
+ driveId,
103
+ syncUnit.syncId,
104
+ revision.revision,
105
+ );
68
106
  }
69
107
 
70
- async processAcknowledge(
71
- driveId: string,
72
- listenerId: string,
73
- revisions: ListenerRevision[]
74
- ): Promise<boolean> {
75
- const syncUnits = await this.manager.getListenerSyncUnitIds(
76
- driveId,
77
- listenerId
78
- );
79
- let success = true;
80
- for (const revision of revisions) {
81
- const syncUnit = syncUnits.find(
82
- s =>
83
- s.scope === revision.scope &&
84
- s.branch === revision.branch &&
85
- s.driveId === revision.driveId &&
86
- s.documentId == revision.documentId
87
- );
88
- if (!syncUnit) {
89
- defaultLogger.warn(
90
- 'Unknown sync unit was acknowledged',
91
- revision
92
- );
93
- success = false;
94
- continue;
95
- }
108
+ return success;
109
+ }
96
110
 
97
- await this.manager.updateListenerRevision(
98
- listenerId,
99
- driveId,
100
- syncUnit.syncId,
101
- revision.revision
102
- );
111
+ static async registerPullResponder(
112
+ driveId: string,
113
+ url: string,
114
+ filter: ListenerFilter,
115
+ ): Promise<Listener["listenerId"]> {
116
+ // graphql request to switchboard
117
+ const result = await requestGraphql<{
118
+ registerPullResponderListener: {
119
+ listenerId: Listener["listenerId"];
120
+ };
121
+ }>(
122
+ url,
123
+ gql`
124
+ mutation registerPullResponderListener($filter: InputListenerFilter!) {
125
+ registerPullResponderListener(filter: $filter) {
126
+ listenerId
127
+ }
103
128
  }
129
+ `,
130
+ { filter },
131
+ );
104
132
 
105
- return success;
133
+ const error = result.errors?.at(0);
134
+ if (error) {
135
+ throw error;
106
136
  }
107
137
 
108
- static async registerPullResponder(
109
- driveId: string,
110
- url: string,
111
- filter: ListenerFilter
112
- ): Promise<Listener['listenerId']> {
113
- // graphql request to switchboard
114
- const { registerPullResponderListener } = await requestGraphql<{
115
- registerPullResponderListener: {
116
- listenerId: Listener['listenerId'];
117
- };
118
- }>(
119
- url,
120
- gql`
121
- mutation registerPullResponderListener(
122
- $filter: InputListenerFilter!
123
- ) {
124
- registerPullResponderListener(filter: $filter) {
125
- listenerId
126
- }
127
- }
128
- `,
129
- { filter }
130
- );
131
- return registerPullResponderListener.listenerId;
138
+ if (!result.registerPullResponderListener) {
139
+ throw new Error("Error registering listener");
132
140
  }
133
141
 
134
- static async pullStrands(
135
- driveId: string,
136
- url: string,
137
- listenerId: string,
138
- since?: string // TODO add support for since
139
- ): Promise<StrandUpdate[]> {
140
- const {
141
- system: {
142
- sync: { strands }
143
- }
144
- } = await requestGraphql<PullStrandsGraphQL>(
145
- url,
146
- gql`
147
- query strands($listenerId: ID!) {
148
- system {
149
- sync {
150
- strands(listenerId: $listenerId) {
151
- driveId
152
- documentId
153
- scope
154
- branch
155
- operations {
156
- id
157
- timestamp
158
- skip
159
- type
160
- input
161
- hash
162
- index
163
- context {
164
- signer {
165
- user {
166
- address
167
- networkId
168
- chainId
169
- }
170
- app {
171
- name
172
- key
173
- }
174
- signature
175
- }
176
- }
177
- }
178
- }
179
- }
142
+ return result.registerPullResponderListener.listenerId;
143
+ }
144
+
145
+ static async pullStrands(
146
+ driveId: string,
147
+ url: string,
148
+ listenerId: string,
149
+ options?: GetStrandsOptions, // TODO add support for since
150
+ ): Promise<StrandUpdate[]> {
151
+ const result = await requestGraphql<PullStrandsGraphQL>(
152
+ url,
153
+ gql`
154
+ query strands($listenerId: ID!) {
155
+ system {
156
+ sync {
157
+ strands(listenerId: $listenerId) {
158
+ driveId
159
+ documentId
160
+ scope
161
+ branch
162
+ operations {
163
+ id
164
+ timestamp
165
+ skip
166
+ type
167
+ input
168
+ hash
169
+ index
170
+ context {
171
+ signer {
172
+ user {
173
+ address
174
+ networkId
175
+ chainId
176
+ }
177
+ app {
178
+ name
179
+ key
180
+ }
181
+ signatures
180
182
  }
183
+ }
181
184
  }
182
- `,
183
- { listenerId }
184
- );
185
- return strands.map(s => ({
186
- ...s,
187
- operations: s.operations.map(o => ({
188
- ...o,
189
- input: JSON.parse(o.input) as object
190
- }))
191
- }));
185
+ }
186
+ }
187
+ }
188
+ }
189
+ `,
190
+ { listenerId },
191
+ );
192
+
193
+ const error = result.errors?.at(0);
194
+ if (error) {
195
+ throw error;
192
196
  }
193
197
 
194
- static async acknowledgeStrands(
195
- driveId: string,
196
- url: string,
197
- listenerId: string,
198
- revisions: ListenerRevision[]
199
- ): Promise<boolean> {
200
- const result = await requestGraphql<{ acknowledge: boolean }>(
201
- url,
202
- gql`
203
- mutation acknowledge(
204
- $listenerId: String!
205
- $revisions: [ListenerRevisionInput]
206
- ) {
207
- acknowledge(listenerId: $listenerId, revisions: $revisions)
208
- }
209
- `,
210
- { listenerId, revisions }
211
- );
212
- return result.acknowledge;
198
+ if (!result.system) {
199
+ return [];
213
200
  }
214
201
 
215
- private static async executePull(
216
- driveId: string,
217
- trigger: PullResponderTrigger,
218
- onStrandUpdate: (strand: StrandUpdate) => Promise<IOperationResult>,
219
- onError: (error: Error) => void,
220
- onRevisions?: (revisions: ListenerRevisionWithError[]) => void,
221
- onAcknowledge?: (success: boolean) => void
222
- ) {
223
- try {
224
- const { url, listenerId } = trigger.data;
225
- const strands = await PullResponderTransmitter.pullStrands(
226
- driveId,
227
- url,
228
- listenerId
229
- // since ?
230
- );
231
-
232
- // if there are no new strands then do nothing
233
- if (!strands.length) {
234
- onRevisions?.([]);
235
- return;
236
- }
202
+ return result.system.sync.strands.map((s) => ({
203
+ ...s,
204
+ operations: s.operations.map((o) => ({
205
+ ...o,
206
+ input: JSON.parse(o.input) as object,
207
+ })),
208
+ }));
209
+ }
210
+
211
+ static async acknowledgeStrands(
212
+ driveId: string,
213
+ url: string,
214
+ listenerId: string,
215
+ revisions: ListenerRevision[],
216
+ ): Promise<boolean> {
217
+ const result = await requestGraphql<{ acknowledge: boolean }>(
218
+ url,
219
+ gql`
220
+ mutation acknowledge(
221
+ $listenerId: String!
222
+ $revisions: [ListenerRevisionInput]
223
+ ) {
224
+ acknowledge(listenerId: $listenerId, revisions: $revisions)
225
+ }
226
+ `,
227
+ { listenerId, revisions },
228
+ );
229
+ const error = result.errors?.at(0);
230
+ if (error) {
231
+ throw error;
232
+ }
237
233
 
238
- const listenerRevisions: ListenerRevisionWithError[] = [];
234
+ if (result.acknowledge === null) {
235
+ throw new Error("Error acknowledging strands");
236
+ }
237
+ return result.acknowledge;
238
+ }
239
239
 
240
- for (const strand of strands) {
241
- let result: IOperationResult | undefined = undefined;
242
- let error: Error | undefined = undefined;
243
- try {
244
- result = await onStrandUpdate(strand);
245
- if (result.error) {
246
- throw result.error;
247
- }
248
- } catch (e) {
249
- error = e as Error;
250
- onError(error);
251
- }
240
+ private static async executePull(
241
+ driveId: string,
242
+ trigger: PullResponderTrigger,
243
+ onStrandUpdate: (
244
+ strand: StrandUpdate,
245
+ source: StrandUpdateSource,
246
+ ) => Promise<IOperationResult>,
247
+ onError: (error: Error) => void,
248
+ onRevisions?: (revisions: ListenerRevisionWithError[]) => void,
249
+ onAcknowledge?: (success: boolean) => void,
250
+ ) {
251
+ try {
252
+ const { url, listenerId } = trigger.data;
253
+ const strands = await PullResponderTransmitter.pullStrands(
254
+ driveId,
255
+ url,
256
+ listenerId,
257
+ // since ?
258
+ );
252
259
 
253
- listenerRevisions.push({
254
- branch: strand.branch,
255
- documentId: strand.documentId || '',
256
- driveId: strand.driveId,
257
- revision:
258
- result?.document?.operations[strand.scope]?.at(-1)
259
- ?.index ?? -1,
260
- scope: strand.scope as OperationScope,
261
- status: error
262
- ? error instanceof OperationError
263
- ? error.status
264
- : 'ERROR'
265
- : 'SUCCESS',
266
- error
267
- });
268
- }
260
+ // if there are no new strands then do nothing
261
+ if (!strands.length) {
262
+ onRevisions?.([]);
263
+ return;
264
+ }
269
265
 
270
- onRevisions?.(listenerRevisions);
271
-
272
- await PullResponderTransmitter.acknowledgeStrands(
273
- driveId,
274
- url,
275
- listenerId,
276
- listenerRevisions.map(revision => {
277
- const { error, ...rest } = revision;
278
- return rest;
279
- })
280
- )
281
- .then(result => onAcknowledge?.(result))
282
- .catch(error => defaultLogger.error('ACK error', error));
283
- } catch (error) {
284
- onError(error as Error);
285
- }
286
- }
266
+ const listenerRevisions: ListenerRevisionWithError[] = [];
287
267
 
288
- static setupPull(
289
- driveId: string,
290
- trigger: PullResponderTrigger,
291
- onStrandUpdate: (strand: StrandUpdate) => Promise<IOperationResult>,
292
- onError: (error: Error) => void,
293
- onRevisions?: (revisions: ListenerRevisionWithError[]) => void,
294
- onAcknowledge?: (success: boolean) => void
295
- ): CancelPullLoop {
296
- const { interval } = trigger.data;
297
- let loopInterval = PULL_DRIVE_INTERVAL;
298
- if (interval) {
299
- try {
300
- const intervalNumber = parseInt(interval);
301
- if (intervalNumber) {
302
- loopInterval = intervalNumber;
303
- }
304
- } catch {
305
- // ignore invalid interval
306
- }
268
+ for (const strand of strands) {
269
+ const operations: Operation[] = strand.operations.map((op) => ({
270
+ ...op,
271
+ scope: strand.scope,
272
+ branch: strand.branch,
273
+ }));
274
+
275
+ let error: Error | undefined = undefined;
276
+ try {
277
+ const result = await onStrandUpdate(strand, {
278
+ type: "trigger",
279
+ trigger,
280
+ });
281
+ if (result.error) {
282
+ throw result.error;
283
+ }
284
+ } catch (e) {
285
+ error = e as Error;
286
+ onError(error);
307
287
  }
308
288
 
309
- let isCancelled = false;
310
- let timeout: number | undefined;
311
-
312
- const executeLoop = async () => {
313
- while (!isCancelled) {
314
- await this.executePull(
315
- driveId,
316
- trigger,
317
- onStrandUpdate,
318
- onError,
319
- onRevisions,
320
- onAcknowledge
321
- );
322
- await new Promise(resolve => {
323
- timeout = setTimeout(
324
- resolve,
325
- loopInterval
326
- ) as unknown as number;
327
- });
328
- }
329
- };
289
+ listenerRevisions.push({
290
+ branch: strand.branch,
291
+ documentId: strand.documentId || "",
292
+ driveId: strand.driveId,
293
+ revision: operations.pop()?.index ?? -1,
294
+ scope: strand.scope,
295
+ status: error
296
+ ? error instanceof OperationError
297
+ ? error.status
298
+ : "ERROR"
299
+ : "SUCCESS",
300
+ error,
301
+ });
302
+ }
330
303
 
331
- executeLoop().catch(defaultLogger.error);
304
+ onRevisions?.(listenerRevisions);
332
305
 
333
- return () => {
334
- isCancelled = true;
335
- if (timeout !== undefined) {
336
- clearTimeout(timeout);
337
- }
338
- };
306
+ await PullResponderTransmitter.acknowledgeStrands(
307
+ driveId,
308
+ url,
309
+ listenerId,
310
+ listenerRevisions.map((revision) => {
311
+ const { error, ...rest } = revision;
312
+ return rest;
313
+ }),
314
+ )
315
+ .then((result) => onAcknowledge?.(result))
316
+ .catch((error) => defaultLogger.error("ACK error", error));
317
+ } catch (error) {
318
+ onError(error as Error);
339
319
  }
320
+ }
340
321
 
341
- static async createPullResponderTrigger(
342
- driveId: string,
343
- url: string,
344
- options: Pick<RemoteDriveOptions, 'pullInterval' | 'pullFilter'>
345
- ): Promise<PullResponderTrigger> {
346
- const { pullFilter, pullInterval } = options;
347
- const listenerId = await PullResponderTransmitter.registerPullResponder(
348
- driveId,
349
- url,
350
- pullFilter ?? {
351
- documentId: ['*'],
352
- documentType: ['*'],
353
- branch: ['*'],
354
- scope: ['*']
355
- }
322
+ static setupPull(
323
+ driveId: string,
324
+ trigger: PullResponderTrigger,
325
+ onStrandUpdate: (
326
+ strand: StrandUpdate,
327
+ source: StrandUpdateSource,
328
+ ) => Promise<IOperationResult>,
329
+ onError: (error: Error) => void,
330
+ onRevisions?: (revisions: ListenerRevisionWithError[]) => void,
331
+ onAcknowledge?: (success: boolean) => void,
332
+ ): CancelPullLoop {
333
+ const { interval } = trigger.data;
334
+ let loopInterval = PULL_DRIVE_INTERVAL;
335
+ if (interval) {
336
+ try {
337
+ const intervalNumber = parseInt(interval);
338
+ if (intervalNumber) {
339
+ loopInterval = intervalNumber;
340
+ }
341
+ } catch {
342
+ // ignore invalid interval
343
+ }
344
+ }
345
+
346
+ let isCancelled = false;
347
+ let timeout: number | undefined;
348
+
349
+ const executeLoop = async () => {
350
+ while (!isCancelled) {
351
+ await this.executePull(
352
+ driveId,
353
+ trigger,
354
+ onStrandUpdate,
355
+ onError,
356
+ onRevisions,
357
+ onAcknowledge,
356
358
  );
359
+ await new Promise((resolve) => {
360
+ timeout = setTimeout(resolve, loopInterval) as unknown as number;
361
+ });
362
+ }
363
+ };
357
364
 
358
- const pullTrigger: PullResponderTrigger = {
359
- id: generateUUID(),
360
- type: 'PullResponder',
361
- data: {
362
- url,
363
- listenerId,
364
- interval: pullInterval?.toString() ?? ''
365
- }
366
- };
367
- return pullTrigger;
368
- }
365
+ executeLoop().catch(defaultLogger.error);
369
366
 
370
- static isPullResponderTrigger(
371
- trigger: Trigger
372
- ): trigger is PullResponderTrigger {
373
- return trigger.type === 'PullResponder';
374
- }
367
+ return () => {
368
+ isCancelled = true;
369
+ if (timeout !== undefined) {
370
+ clearTimeout(timeout);
371
+ }
372
+ };
373
+ }
374
+
375
+ static async createPullResponderTrigger(
376
+ driveId: string,
377
+ url: string,
378
+ options: Pick<RemoteDriveOptions, "pullInterval" | "pullFilter">,
379
+ ): Promise<PullResponderTrigger> {
380
+ const { pullFilter, pullInterval } = options;
381
+ const listenerId = await PullResponderTransmitter.registerPullResponder(
382
+ driveId,
383
+ url,
384
+ pullFilter ?? {
385
+ documentId: ["*"],
386
+ documentType: ["*"],
387
+ branch: ["*"],
388
+ scope: ["*"],
389
+ },
390
+ );
391
+
392
+ const pullTrigger: PullResponderTrigger = {
393
+ id: generateUUID(),
394
+ type: "PullResponder",
395
+ data: {
396
+ url,
397
+ listenerId,
398
+ interval: pullInterval?.toString() ?? "",
399
+ },
400
+ };
401
+ return pullTrigger;
402
+ }
403
+
404
+ static isPullResponderTrigger(
405
+ trigger: Trigger,
406
+ ): trigger is PullResponderTrigger {
407
+ return trigger.type === "PullResponder";
408
+ }
375
409
  }