@apibara/protocol 0.1.2 → 0.3.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.
package/dist/buffer.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Converts the hex-encoded bytes to a `Buffer`.
4
4
  * @param hex The hex string
5
- * @param size The buffer size, in bits
5
+ * @param size The buffer size, in bytes
6
6
  */
7
7
  export declare function hexToBuffer(hex: string, size: number): Buffer;
8
8
  /**
package/dist/buffer.js CHANGED
@@ -4,7 +4,7 @@ exports.bufferToHex = exports.hexToBuffer = void 0;
4
4
  /**
5
5
  * Converts the hex-encoded bytes to a `Buffer`.
6
6
  * @param hex The hex string
7
- * @param size The buffer size, in bits
7
+ * @param size The buffer size, in bytes
8
8
  */
9
9
  function hexToBuffer(hex, size) {
10
10
  const padSize = size * 2;
package/dist/client.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { ClientOptions, ChannelCredentials, ClientReadableStream } from '@grpc/grpc-js';
1
+ import { ClientOptions, ChannelCredentials } from '@grpc/grpc-js';
2
+ import { Retry, StreamMessagesStream } from './stream';
2
3
  import { NodeClient as GrpcNodeClient } from './proto/apibara/node/v1alpha1/Node';
3
4
  import { StatusResponse__Output } from './proto/apibara/node/v1alpha1/StatusResponse';
4
- import { StreamMessagesResponse__Output } from './proto/apibara/node/v1alpha1/StreamMessagesResponse';
5
5
  import { StreamMessagesRequest } from './proto/apibara/node/v1alpha1/StreamMessagesRequest';
6
6
  export declare const Node: (new (address: string, credentials: ChannelCredentials, options?: ClientOptions | undefined) => GrpcNodeClient) & {
7
7
  service: import("./proto/apibara/node/v1alpha1/Node").NodeDefinition;
@@ -16,9 +16,13 @@ export declare const credentials: {
16
16
  createFromGoogleCredential: typeof import("@grpc/grpc-js").CallCredentials.createFromGoogleCredential;
17
17
  createEmpty: typeof import("@grpc/grpc-js").CallCredentials.createEmpty;
18
18
  };
19
+ export interface StreamMessagesOptions {
20
+ reconnect?: boolean;
21
+ onRetry?: (retryCount: number) => Retry;
22
+ }
19
23
  export declare class NodeClient {
20
24
  private readonly client;
21
25
  constructor(address: string, credentials: ChannelCredentials, options?: ClientOptions);
22
26
  status(): Promise<StatusResponse__Output | undefined>;
23
- streamMessages(args: StreamMessagesRequest): ClientReadableStream<StreamMessagesResponse__Output>;
27
+ streamMessages(args: StreamMessagesRequest, options?: StreamMessagesOptions): StreamMessagesStream;
24
28
  }
package/dist/client.js CHANGED
@@ -13,6 +13,7 @@ exports.NodeClient = exports.credentials = exports.Node = void 0;
13
13
  const util_1 = require("util");
14
14
  const grpc_js_1 = require("@grpc/grpc-js");
15
15
  const proto_loader_1 = require("@grpc/proto-loader");
16
+ const stream_1 = require("./stream");
16
17
  const __NODE_PROTO_PATH = __dirname + '/proto/node.proto';
17
18
  const packageDefinition = (0, proto_loader_1.loadSync)(__NODE_PROTO_PATH, {});
18
19
  const protoDescriptor = (0, grpc_js_1.loadPackageDefinition)(packageDefinition);
@@ -27,8 +28,18 @@ class NodeClient {
27
28
  return (0, util_1.promisify)(this.client.Status.bind(this.client, {}))();
28
29
  });
29
30
  }
30
- streamMessages(args) {
31
- return this.client.streamMessages(args);
31
+ streamMessages(args, options) {
32
+ // only reconnect if user has opted-in
33
+ let onRetry = stream_1.neverRetry;
34
+ if (options === null || options === void 0 ? void 0 : options.reconnect) {
35
+ if (options.onRetry) {
36
+ onRetry = options.onRetry;
37
+ }
38
+ else {
39
+ onRetry = stream_1.defaultOnRetry;
40
+ }
41
+ }
42
+ return new stream_1.StreamMessagesStream({ args, onRetry, client: this.client });
32
43
  }
33
44
  }
34
45
  exports.NodeClient = NodeClient;
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,+BAAgC;AAChC,2CAMsB;AACtB,qDAA6C;AAO7C,MAAM,iBAAiB,GAAG,SAAS,GAAG,mBAAmB,CAAA;AAEzD,MAAM,iBAAiB,GAAG,IAAA,uBAAQ,EAAC,iBAAiB,EAAE,EAAE,CAAC,CAAA;AACzD,MAAM,eAAe,GAAG,IAAA,+BAAqB,EAAC,iBAAiB,CAA6B,CAAA;AAE/E,QAAA,IAAI,GAAG,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAA;AAEjD,QAAA,WAAW,GAAG,qBAAe,CAAA;AAE1C,MAAa,UAAU;IAGrB,YAAY,OAAe,EAAE,WAA+B,EAAE,OAAuB;QACnF,IAAI,CAAC,MAAM,GAAG,IAAI,YAAI,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,CAAC,CAAA;IACvD,CAAC;IAEY,MAAM;;YACjB,OAAO,IAAA,gBAAS,EAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,CAAA;QAC9D,CAAC;KAAA;IAEM,cAAc,CACnB,IAA2B;QAE3B,OAAO,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAA;IACzC,CAAC;CACF;AAhBD,gCAgBC"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,+BAAgC;AAChC,2CAKsB;AACtB,qDAA6C;AAC7C,qCAAkF;AAMlF,MAAM,iBAAiB,GAAG,SAAS,GAAG,mBAAmB,CAAA;AAEzD,MAAM,iBAAiB,GAAG,IAAA,uBAAQ,EAAC,iBAAiB,EAAE,EAAE,CAAC,CAAA;AACzD,MAAM,eAAe,GAAG,IAAA,+BAAqB,EAAC,iBAAiB,CAA6B,CAAA;AAE/E,QAAA,IAAI,GAAG,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAA;AAEjD,QAAA,WAAW,GAAG,qBAAe,CAAA;AAO1C,MAAa,UAAU;IAGrB,YAAY,OAAe,EAAE,WAA+B,EAAE,OAAuB;QACnF,IAAI,CAAC,MAAM,GAAG,IAAI,YAAI,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,CAAC,CAAA;IACvD,CAAC;IAEY,MAAM;;YACjB,OAAO,IAAA,gBAAS,EAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,CAAA;QAC9D,CAAC;KAAA;IAEM,cAAc,CACnB,IAA2B,EAC3B,OAA+B;QAE/B,sCAAsC;QACtC,IAAI,OAAO,GAAG,mBAAU,CAAA;QACxB,IAAI,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,SAAS,EAAE;YACtB,IAAI,OAAO,CAAC,OAAO,EAAE;gBACnB,OAAO,GAAG,OAAO,CAAC,OAAO,CAAA;aAC1B;iBAAM;gBACL,OAAO,GAAG,uBAAc,CAAA;aACzB;SACF;QAED,OAAO,IAAI,6BAAoB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;IACzE,CAAC;CACF;AA3BD,gCA2BC"}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './client';
2
2
  export * from './buffer';
3
+ export * from './stream';
3
4
  export * as proto from './proto';
package/dist/index.js CHANGED
@@ -29,5 +29,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
29
29
  exports.proto = void 0;
30
30
  __exportStar(require("./client"), exports);
31
31
  __exportStar(require("./buffer"), exports);
32
+ __exportStar(require("./stream"), exports);
32
33
  exports.proto = __importStar(require("./proto"));
33
34
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAwB;AACxB,2CAAwB;AACxB,iDAAgC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAwB;AACxB,2CAAwB;AACxB,2CAAwB;AACxB,iDAAgC"}
@@ -1,7 +1,9 @@
1
1
  import type { Long } from '@grpc/proto-loader';
2
2
  export interface StreamMessagesRequest {
3
3
  'startingSequence'?: (number | string | Long);
4
+ 'pendingBlockIntervalSeconds'?: (number);
4
5
  }
5
6
  export interface StreamMessagesRequest__Output {
6
7
  'startingSequence': (string);
8
+ 'pendingBlockIntervalSeconds': (number);
7
9
  }
@@ -5,11 +5,13 @@ export interface StreamMessagesResponse {
5
5
  'invalidate'?: (_apibara_node_v1alpha1_Invalidate | null);
6
6
  'data'?: (_apibara_node_v1alpha1_Data | null);
7
7
  'heartbeat'?: (_apibara_node_v1alpha1_Heartbeat | null);
8
- 'message'?: "invalidate" | "data" | "heartbeat";
8
+ 'pending'?: (_apibara_node_v1alpha1_Data | null);
9
+ 'message'?: "invalidate" | "data" | "heartbeat" | "pending";
9
10
  }
10
11
  export interface StreamMessagesResponse__Output {
11
12
  'invalidate'?: (_apibara_node_v1alpha1_Invalidate__Output | null);
12
13
  'data'?: (_apibara_node_v1alpha1_Data__Output | null);
13
14
  'heartbeat'?: (_apibara_node_v1alpha1_Heartbeat__Output | null);
14
- 'message': "invalidate" | "data" | "heartbeat";
15
+ 'pending'?: (_apibara_node_v1alpha1_Data__Output | null);
16
+ 'message': "invalidate" | "data" | "heartbeat" | "pending";
15
17
  }
@@ -70,6 +70,8 @@ message ConnectResponse {
70
70
  message StreamMessagesRequest {
71
71
  // Start streaming from the provided sequence number.
72
72
  uint64 starting_sequence = 1;
73
+ // If greater than 0, send pending blocks at the specified interval.
74
+ uint32 pending_block_interval_seconds = 2;
73
75
  }
74
76
 
75
77
  // Message sent from the node to the client.
@@ -78,6 +80,7 @@ message StreamMessagesResponse {
78
80
  Invalidate invalidate = 1;
79
81
  Data data = 2;
80
82
  Heartbeat heartbeat = 3;
83
+ Data pending = 4;
81
84
  }
82
85
  }
83
86
 
@@ -0,0 +1,32 @@
1
+ /// <reference types="node" />
2
+ import { Readable } from 'stream';
3
+ import { NodeClient as GrpcNodeClient } from './proto/apibara/node/v1alpha1/Node';
4
+ import { StreamMessagesResponse__Output } from './proto/apibara/node/v1alpha1/StreamMessagesResponse';
5
+ import { StreamMessagesRequest } from './proto/apibara/node/v1alpha1/StreamMessagesRequest';
6
+ export declare function neverRetry(_retryCount: number): Retry;
7
+ export declare function defaultOnRetry(retryCount: number): Retry;
8
+ export interface Retry {
9
+ retry: boolean;
10
+ startingSequence?: number;
11
+ delay?: number;
12
+ }
13
+ export declare class StreamMessagesStream extends Readable {
14
+ private readonly client;
15
+ private readonly initialRequest;
16
+ private readonly onRetry;
17
+ private source?;
18
+ private currentSequence?;
19
+ private retryCount;
20
+ constructor({ args, onRetry, client, }: {
21
+ args: StreamMessagesRequest;
22
+ onRetry: (retryCount: number) => Retry;
23
+ client: GrpcNodeClient;
24
+ });
25
+ _construct(callback: (error?: Error | null | undefined) => void): void;
26
+ _read(_size: number): void;
27
+ setupSource(startingSequence?: number): void;
28
+ cleanupSource(): void;
29
+ onData(chunk: StreamMessagesResponse__Output): void;
30
+ onError(_err: any): void;
31
+ onEnd(): void;
32
+ }
package/dist/stream.js ADDED
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StreamMessagesStream = exports.defaultOnRetry = exports.neverRetry = void 0;
4
+ const stream_1 = require("stream");
5
+ const defaultMaxRetry = 3;
6
+ const defaultDelaySeconds = 5;
7
+ function neverRetry(_retryCount) {
8
+ return {
9
+ retry: false,
10
+ };
11
+ }
12
+ exports.neverRetry = neverRetry;
13
+ function defaultOnRetry(retryCount) {
14
+ return {
15
+ retry: retryCount < defaultMaxRetry,
16
+ delay: defaultDelaySeconds,
17
+ };
18
+ }
19
+ exports.defaultOnRetry = defaultOnRetry;
20
+ class StreamMessagesStream extends stream_1.Readable {
21
+ constructor({ args, onRetry, client, }) {
22
+ super({ objectMode: true });
23
+ this.initialRequest = args;
24
+ this.client = client;
25
+ this.onRetry = onRetry;
26
+ this.retryCount = 0;
27
+ }
28
+ _construct(callback) {
29
+ this.setupSource();
30
+ callback();
31
+ }
32
+ _read(_size) {
33
+ if (this.source) {
34
+ // resume source data if not flowing
35
+ if (!this.source.readableFlowing) {
36
+ this.source.resume();
37
+ }
38
+ }
39
+ else {
40
+ // create source stream
41
+ this.setupSource(this.currentSequence);
42
+ }
43
+ }
44
+ setupSource(startingSequence) {
45
+ const request = startingSequence !== undefined
46
+ ? Object.assign(Object.assign({}, this.initialRequest), { startingSequence }) : this.initialRequest;
47
+ this.source = this.client.streamMessages(request);
48
+ this.source.on('data', this.onData.bind(this));
49
+ this.source.once('error', this.onError.bind(this));
50
+ this.source.once('end', this.onEnd.bind(this));
51
+ }
52
+ cleanupSource() {
53
+ if (!this.source) {
54
+ return;
55
+ }
56
+ this.source.off('data', this.onData.bind(this));
57
+ this.source.off('error', this.onData.bind(this));
58
+ this.source.off('end', this.onEnd.bind(this));
59
+ }
60
+ onData(chunk) {
61
+ var _a;
62
+ // reset retry count
63
+ this.retryCount = 0;
64
+ if (chunk.data) {
65
+ this.currentSequence = +chunk.data.sequence;
66
+ }
67
+ else if (chunk.invalidate) {
68
+ this.currentSequence = +chunk.invalidate;
69
+ }
70
+ // apply backpressure on stream
71
+ if (!this.push(chunk)) {
72
+ (_a = this.source) === null || _a === void 0 ? void 0 : _a.pause();
73
+ }
74
+ }
75
+ onError(_err) {
76
+ this.cleanupSource();
77
+ this.retryCount += 1;
78
+ const { retry, delay, startingSequence } = this.onRetry(this.retryCount);
79
+ if (retry) {
80
+ const intervalDelayMs = (delay !== null && delay !== void 0 ? delay : defaultDelaySeconds) * 1000;
81
+ setTimeout(() => {
82
+ this.setupSource(startingSequence);
83
+ }, intervalDelayMs);
84
+ }
85
+ }
86
+ onEnd() {
87
+ // block streams never end
88
+ if (this.retryCount > 0) {
89
+ return;
90
+ }
91
+ this.cleanupSource();
92
+ }
93
+ }
94
+ exports.StreamMessagesStream = StreamMessagesStream;
95
+ //# sourceMappingURL=stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"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"}
File without changes
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ describe('StreamMessagesStream', () => {
12
+ it('produces data', () => __awaiter(void 0, void 0, void 0, function* () { }));
13
+ describe('on error', () => {
14
+ it('produces an error by default', () => { });
15
+ it('reconnects', () => { });
16
+ it('produces an error after trying to reconnect', () => { });
17
+ });
18
+ });
19
+ //# sourceMappingURL=stream.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.test.js","sourceRoot":"","sources":["../src/stream.test.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,eAAe,EAAE,GAAS,EAAE,kDAAE,CAAC,CAAA,CAAC,CAAA;IAEnC,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;QACxB,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;QAC5C,EAAE,CAAC,YAAY,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;QAC1B,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IAC7D,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apibara/protocol",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "source": "src/index.ts",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -20,6 +20,7 @@
20
20
  "scripts": {
21
21
  "prebuild": "pnpm run protobuf && pnpm run gen-types",
22
22
  "build": "tsc",
23
+ "test": "jest",
23
24
  "lint": "eslint src --ext .ts",
24
25
  "lint:fix": "eslint --fix src --ext .ts",
25
26
  "protobuf": "mkdir -p dist/proto && cp src/proto/*.proto dist/proto",
package/src/buffer.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Converts the hex-encoded bytes to a `Buffer`.
3
3
  * @param hex The hex string
4
- * @param size The buffer size, in bits
4
+ * @param size The buffer size, in bytes
5
5
  */
6
6
  export function hexToBuffer(hex: string, size: number): Buffer {
7
7
  const padSize = size * 2
package/src/client.ts CHANGED
@@ -4,13 +4,12 @@ import {
4
4
  credentials as grpcCredentials,
5
5
  ClientOptions,
6
6
  ChannelCredentials,
7
- ClientReadableStream,
8
7
  } from '@grpc/grpc-js'
9
8
  import { loadSync } from '@grpc/proto-loader'
9
+ import { Retry, neverRetry, defaultOnRetry, StreamMessagesStream } from './stream'
10
10
  import { NodeClient as GrpcNodeClient } from './proto/apibara/node/v1alpha1/Node'
11
11
  import { ProtoGrpcType } from './proto/node'
12
12
  import { StatusResponse__Output } from './proto/apibara/node/v1alpha1/StatusResponse'
13
- import { StreamMessagesResponse__Output } from './proto/apibara/node/v1alpha1/StreamMessagesResponse'
14
13
  import { StreamMessagesRequest } from './proto/apibara/node/v1alpha1/StreamMessagesRequest'
15
14
 
16
15
  const __NODE_PROTO_PATH = __dirname + '/proto/node.proto'
@@ -22,6 +21,11 @@ export const Node = protoDescriptor.apibara.node.v1alpha1.Node
22
21
 
23
22
  export const credentials = grpcCredentials
24
23
 
24
+ export interface StreamMessagesOptions {
25
+ reconnect?: boolean
26
+ onRetry?: (retryCount: number) => Retry
27
+ }
28
+
25
29
  export class NodeClient {
26
30
  private readonly client: GrpcNodeClient
27
31
 
@@ -34,8 +38,19 @@ export class NodeClient {
34
38
  }
35
39
 
36
40
  public streamMessages(
37
- args: StreamMessagesRequest
38
- ): ClientReadableStream<StreamMessagesResponse__Output> {
39
- return this.client.streamMessages(args)
41
+ args: StreamMessagesRequest,
42
+ options?: StreamMessagesOptions
43
+ ): StreamMessagesStream {
44
+ // only reconnect if user has opted-in
45
+ let onRetry = neverRetry
46
+ if (options?.reconnect) {
47
+ if (options.onRetry) {
48
+ onRetry = options.onRetry
49
+ } else {
50
+ onRetry = defaultOnRetry
51
+ }
52
+ }
53
+
54
+ return new StreamMessagesStream({ args, onRetry, client: this.client })
40
55
  }
41
56
  }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './client'
2
2
  export * from './buffer'
3
+ export * from './stream'
3
4
  export * as proto from './proto'
@@ -4,8 +4,10 @@ import type { Long } from '@grpc/proto-loader';
4
4
 
5
5
  export interface StreamMessagesRequest {
6
6
  'startingSequence'?: (number | string | Long);
7
+ 'pendingBlockIntervalSeconds'?: (number);
7
8
  }
8
9
 
9
10
  export interface StreamMessagesRequest__Output {
10
11
  'startingSequence': (string);
12
+ 'pendingBlockIntervalSeconds': (number);
11
13
  }
@@ -8,12 +8,14 @@ export interface StreamMessagesResponse {
8
8
  'invalidate'?: (_apibara_node_v1alpha1_Invalidate | null);
9
9
  'data'?: (_apibara_node_v1alpha1_Data | null);
10
10
  'heartbeat'?: (_apibara_node_v1alpha1_Heartbeat | null);
11
- 'message'?: "invalidate"|"data"|"heartbeat";
11
+ 'pending'?: (_apibara_node_v1alpha1_Data | null);
12
+ 'message'?: "invalidate"|"data"|"heartbeat"|"pending";
12
13
  }
13
14
 
14
15
  export interface StreamMessagesResponse__Output {
15
16
  'invalidate'?: (_apibara_node_v1alpha1_Invalidate__Output | null);
16
17
  'data'?: (_apibara_node_v1alpha1_Data__Output | null);
17
18
  'heartbeat'?: (_apibara_node_v1alpha1_Heartbeat__Output | null);
18
- 'message': "invalidate"|"data"|"heartbeat";
19
+ 'pending'?: (_apibara_node_v1alpha1_Data__Output | null);
20
+ 'message': "invalidate"|"data"|"heartbeat"|"pending";
19
21
  }
@@ -70,6 +70,8 @@ message ConnectResponse {
70
70
  message StreamMessagesRequest {
71
71
  // Start streaming from the provided sequence number.
72
72
  uint64 starting_sequence = 1;
73
+ // If greater than 0, send pending blocks at the specified interval.
74
+ uint32 pending_block_interval_seconds = 2;
73
75
  }
74
76
 
75
77
  // Message sent from the node to the client.
@@ -78,6 +80,7 @@ message StreamMessagesResponse {
78
80
  Invalidate invalidate = 1;
79
81
  Data data = 2;
80
82
  Heartbeat heartbeat = 3;
83
+ Data pending = 4;
81
84
  }
82
85
  }
83
86
 
@@ -0,0 +1,9 @@
1
+ describe('StreamMessagesStream', () => {
2
+ it('produces data', async () => {})
3
+
4
+ describe('on error', () => {
5
+ it('produces an error by default', () => {})
6
+ it('reconnects', () => {})
7
+ it('produces an error after trying to reconnect', () => {})
8
+ })
9
+ })
package/src/stream.ts ADDED
@@ -0,0 +1,127 @@
1
+ import { Readable } from 'stream'
2
+ import { ClientReadableStream } from '@grpc/grpc-js'
3
+ import { NodeClient as GrpcNodeClient } from './proto/apibara/node/v1alpha1/Node'
4
+ import { StreamMessagesResponse__Output } from './proto/apibara/node/v1alpha1/StreamMessagesResponse'
5
+ import { StreamMessagesRequest } from './proto/apibara/node/v1alpha1/StreamMessagesRequest'
6
+
7
+ const defaultMaxRetry = 3
8
+ const defaultDelaySeconds = 5
9
+
10
+ export function neverRetry(_retryCount: number): Retry {
11
+ return {
12
+ retry: false,
13
+ }
14
+ }
15
+
16
+ export function defaultOnRetry(retryCount: number): Retry {
17
+ return {
18
+ retry: retryCount < defaultMaxRetry,
19
+ delay: defaultDelaySeconds,
20
+ }
21
+ }
22
+
23
+ export interface Retry {
24
+ retry: boolean
25
+ startingSequence?: number
26
+ delay?: number
27
+ }
28
+
29
+ export class StreamMessagesStream extends Readable {
30
+ private readonly client: GrpcNodeClient
31
+ private readonly initialRequest: StreamMessagesRequest
32
+ private readonly onRetry: (retryCount: number) => Retry
33
+ private source?: ClientReadableStream<StreamMessagesResponse__Output>
34
+ private currentSequence?: number
35
+ private retryCount: number
36
+
37
+ constructor({
38
+ args,
39
+ onRetry,
40
+ client,
41
+ }: {
42
+ args: StreamMessagesRequest
43
+ onRetry: (retryCount: number) => Retry
44
+ client: GrpcNodeClient
45
+ }) {
46
+ super({ objectMode: true })
47
+ this.initialRequest = args
48
+ this.client = client
49
+ this.onRetry = onRetry
50
+ this.retryCount = 0
51
+ }
52
+
53
+ _construct(callback: (error?: Error | null | undefined) => void): void {
54
+ this.setupSource()
55
+ callback()
56
+ }
57
+
58
+ _read(_size: number): void {
59
+ if (this.source) {
60
+ // resume source data if not flowing
61
+ if (!this.source.readableFlowing) {
62
+ this.source.resume()
63
+ }
64
+ } else {
65
+ // create source stream
66
+ this.setupSource(this.currentSequence)
67
+ }
68
+ }
69
+
70
+ setupSource(startingSequence?: number) {
71
+ const request =
72
+ startingSequence !== undefined
73
+ ? { ...this.initialRequest, startingSequence }
74
+ : this.initialRequest
75
+ this.source = this.client.streamMessages(request)
76
+
77
+ this.source.on('data', this.onData.bind(this))
78
+ this.source.once('error', this.onError.bind(this))
79
+ this.source.once('end', this.onEnd.bind(this))
80
+ }
81
+
82
+ cleanupSource() {
83
+ if (!this.source) {
84
+ return
85
+ }
86
+
87
+ this.source.off('data', this.onData.bind(this))
88
+ this.source.off('error', this.onData.bind(this))
89
+ this.source.off('end', this.onEnd.bind(this))
90
+ }
91
+
92
+ onData(chunk: StreamMessagesResponse__Output) {
93
+ // reset retry count
94
+ this.retryCount = 0
95
+ if (chunk.data) {
96
+ this.currentSequence = +chunk.data.sequence
97
+ } else if (chunk.invalidate) {
98
+ this.currentSequence = +chunk.invalidate
99
+ }
100
+
101
+ // apply backpressure on stream
102
+ if (!this.push(chunk)) {
103
+ this.source?.pause()
104
+ }
105
+ }
106
+
107
+ onError(_err: any) {
108
+ this.cleanupSource()
109
+ this.retryCount += 1
110
+
111
+ const { retry, delay, startingSequence } = this.onRetry(this.retryCount)
112
+ if (retry) {
113
+ const intervalDelayMs = (delay ?? defaultDelaySeconds) * 1000
114
+ setTimeout(() => {
115
+ this.setupSource(startingSequence)
116
+ }, intervalDelayMs)
117
+ }
118
+ }
119
+
120
+ onEnd() {
121
+ // block streams never end
122
+ if (this.retryCount > 0) {
123
+ return
124
+ }
125
+ this.cleanupSource()
126
+ }
127
+ }