@apibara/indexer 2.0.0-beta.20 → 2.0.0-beta.22

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/index.cjs CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const protocol = require('@apibara/protocol');
3
4
  const consola = require('consola');
4
5
  const hookable = require('hookable');
5
6
  const assert = require('node:assert');
@@ -39,7 +40,42 @@ function createIndexer({
39
40
  }
40
41
  return indexer;
41
42
  }
42
- async function run(client, indexer) {
43
+ async function runWithReconnect(client, indexer, options = {}) {
44
+ let retryCount = 0;
45
+ const maxRetries = options.maxRetries ?? 10;
46
+ const retryDelay = options.retryDelay ?? 1e3;
47
+ const maxWait = options.maxWait ?? 3e4;
48
+ const runOptions = {
49
+ onConnect() {
50
+ retryCount = 0;
51
+ }
52
+ };
53
+ while (true) {
54
+ try {
55
+ await run(client, indexer, runOptions);
56
+ return;
57
+ } catch (error) {
58
+ retryCount++;
59
+ if (error instanceof protocol.ClientError) {
60
+ if (error.code === protocol.Status.INTERNAL) {
61
+ if (retryCount < maxRetries) {
62
+ consola__default.error(
63
+ "Internal server error, reconnecting...",
64
+ error.message
65
+ );
66
+ const delay = Math.random() * (retryDelay * 0.2) + retryDelay;
67
+ await new Promise(
68
+ (resolve) => setTimeout(resolve, Math.min(retryCount * delay, maxWait))
69
+ );
70
+ continue;
71
+ }
72
+ }
73
+ }
74
+ throw error;
75
+ }
76
+ }
77
+ }
78
+ async function run(client, indexer, runOptions = {}) {
43
79
  await context.indexerAsyncContext.callAsync({}, async () => {
44
80
  const context$1 = context.useIndexerContext();
45
81
  const sink$1 = indexer.options.sink ?? sink.defaultSink();
@@ -60,11 +96,18 @@ async function run(client, indexer) {
60
96
  }
61
97
  let stream = client.streamData(request, options)[Symbol.asyncIterator]();
62
98
  await indexer.hooks.callHook("connect:after");
99
+ let onConnectCalled = false;
63
100
  while (true) {
64
101
  const { value: message, done } = await stream.next();
65
102
  if (done) {
66
103
  break;
67
104
  }
105
+ if (!onConnectCalled) {
106
+ onConnectCalled = true;
107
+ if (runOptions.onConnect) {
108
+ await runOptions.onConnect();
109
+ }
110
+ }
68
111
  await indexer.hooks.callHook("message", { message });
69
112
  switch (message._tag) {
70
113
  case "data": {
@@ -212,3 +255,4 @@ exports.useSink = sink.useSink;
212
255
  exports.createIndexer = createIndexer;
213
256
  exports.defineIndexer = defineIndexer;
214
257
  exports.run = run;
258
+ exports.runWithReconnect = runWithReconnect;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- export { b as Indexer, e as IndexerConfig, c as IndexerHooks, I as IndexerWithStreamConfig, g as createIndexer, f as defineIndexer, r as run } from './shared/indexer.f97f8e70.cjs';
1
+ export { b as Indexer, e as IndexerConfig, c as IndexerHooks, I as IndexerWithStreamConfig, R as ReconnectOptions, h as RunOptions, g as createIndexer, f as defineIndexer, i as run, r as runWithReconnect } from './shared/indexer.3bc6abe3.cjs';
2
2
  export { D as DefaultSink, S as Sink, a as SinkCursorParams, b as SinkData, d as defaultSink, u as useIndexerContext, c as useSink } from './shared/indexer.47b4546b.cjs';
3
3
  import '@apibara/protocol';
4
4
  import 'hookable';
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { b as Indexer, e as IndexerConfig, c as IndexerHooks, I as IndexerWithStreamConfig, g as createIndexer, f as defineIndexer, r as run } from './shared/indexer.6f70ff10.mjs';
1
+ export { b as Indexer, e as IndexerConfig, c as IndexerHooks, I as IndexerWithStreamConfig, R as ReconnectOptions, h as RunOptions, g as createIndexer, f as defineIndexer, i as run, r as runWithReconnect } from './shared/indexer.b4be75f1.mjs';
2
2
  export { D as DefaultSink, S as Sink, a as SinkCursorParams, b as SinkData, d as defaultSink, u as useIndexerContext, c as useSink } from './shared/indexer.47b4546b.mjs';
3
3
  import '@apibara/protocol';
4
4
  import 'hookable';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { b as Indexer, e as IndexerConfig, c as IndexerHooks, I as IndexerWithStreamConfig, g as createIndexer, f as defineIndexer, r as run } from './shared/indexer.5a040ab3.js';
1
+ export { b as Indexer, e as IndexerConfig, c as IndexerHooks, I as IndexerWithStreamConfig, R as ReconnectOptions, h as RunOptions, g as createIndexer, f as defineIndexer, i as run, r as runWithReconnect } from './shared/indexer.a05207df.js';
2
2
  export { D as DefaultSink, S as Sink, a as SinkCursorParams, b as SinkData, d as defaultSink, u as useIndexerContext, c as useSink } from './shared/indexer.47b4546b.js';
3
3
  import '@apibara/protocol';
4
4
  import 'hookable';
package/dist/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { ClientError, Status } from '@apibara/protocol';
1
2
  import consola from 'consola';
2
3
  import { createHooks, createDebugger } from 'hookable';
3
4
  import assert from 'node:assert';
@@ -33,7 +34,42 @@ function createIndexer({
33
34
  }
34
35
  return indexer;
35
36
  }
36
- async function run(client, indexer) {
37
+ async function runWithReconnect(client, indexer, options = {}) {
38
+ let retryCount = 0;
39
+ const maxRetries = options.maxRetries ?? 10;
40
+ const retryDelay = options.retryDelay ?? 1e3;
41
+ const maxWait = options.maxWait ?? 3e4;
42
+ const runOptions = {
43
+ onConnect() {
44
+ retryCount = 0;
45
+ }
46
+ };
47
+ while (true) {
48
+ try {
49
+ await run(client, indexer, runOptions);
50
+ return;
51
+ } catch (error) {
52
+ retryCount++;
53
+ if (error instanceof ClientError) {
54
+ if (error.code === Status.INTERNAL) {
55
+ if (retryCount < maxRetries) {
56
+ consola.error(
57
+ "Internal server error, reconnecting...",
58
+ error.message
59
+ );
60
+ const delay = Math.random() * (retryDelay * 0.2) + retryDelay;
61
+ await new Promise(
62
+ (resolve) => setTimeout(resolve, Math.min(retryCount * delay, maxWait))
63
+ );
64
+ continue;
65
+ }
66
+ }
67
+ }
68
+ throw error;
69
+ }
70
+ }
71
+ }
72
+ async function run(client, indexer, runOptions = {}) {
37
73
  await indexerAsyncContext.callAsync({}, async () => {
38
74
  const context = useIndexerContext();
39
75
  const sink = indexer.options.sink ?? defaultSink();
@@ -54,11 +90,18 @@ async function run(client, indexer) {
54
90
  }
55
91
  let stream = client.streamData(request, options)[Symbol.asyncIterator]();
56
92
  await indexer.hooks.callHook("connect:after");
93
+ let onConnectCalled = false;
57
94
  while (true) {
58
95
  const { value: message, done } = await stream.next();
59
96
  if (done) {
60
97
  break;
61
98
  }
99
+ if (!onConnectCalled) {
100
+ onConnectCalled = true;
101
+ if (runOptions.onConnect) {
102
+ await runOptions.onConnect();
103
+ }
104
+ }
62
105
  await indexer.hooks.callHook("message", { message });
63
106
  switch (message._tag) {
64
107
  case "data": {
@@ -198,4 +241,4 @@ async function run(client, indexer) {
198
241
  });
199
242
  }
200
243
 
201
- export { createIndexer, defaultSink, defineIndexer, run, useIndexerContext };
244
+ export { createIndexer, defaultSink, defineIndexer, run, runWithReconnect, useIndexerContext };
@@ -1,4 +1,4 @@
1
- export { a as IndexerPlugin, d as defineIndexerPlugin } from '../shared/indexer.f97f8e70.cjs';
1
+ export { a as IndexerPlugin, d as defineIndexerPlugin } from '../shared/indexer.3bc6abe3.cjs';
2
2
  import '@apibara/protocol';
3
3
  import 'hookable';
4
4
  import '../shared/indexer.47b4546b.cjs';
@@ -1,4 +1,4 @@
1
- export { a as IndexerPlugin, d as defineIndexerPlugin } from '../shared/indexer.6f70ff10.mjs';
1
+ export { a as IndexerPlugin, d as defineIndexerPlugin } from '../shared/indexer.b4be75f1.mjs';
2
2
  import '@apibara/protocol';
3
3
  import 'hookable';
4
4
  import '../shared/indexer.47b4546b.mjs';
@@ -1,4 +1,4 @@
1
- export { a as IndexerPlugin, d as defineIndexerPlugin } from '../shared/indexer.5a040ab3.js';
1
+ export { a as IndexerPlugin, d as defineIndexerPlugin } from '../shared/indexer.a05207df.js';
2
2
  import '@apibara/protocol';
3
3
  import 'hookable';
4
4
  import '../shared/indexer.47b4546b.js';
@@ -6,6 +6,7 @@ const helper = require('../shared/indexer.219f58e0.cjs');
6
6
  require('node:fs/promises');
7
7
  require('node:path');
8
8
  require('klona/full');
9
+ require('@apibara/protocol');
9
10
  require('consola');
10
11
  require('hookable');
11
12
  require('../shared/indexer.943adee7.cjs');
@@ -1,4 +1,4 @@
1
- import { a as IndexerPlugin } from '../shared/indexer.f97f8e70.cjs';
1
+ import { a as IndexerPlugin } from '../shared/indexer.3bc6abe3.cjs';
2
2
  import { DataFinality, Cursor } from '@apibara/protocol';
3
3
  import { Database } from 'better-sqlite3';
4
4
  import 'hookable';
@@ -1,4 +1,4 @@
1
- import { a as IndexerPlugin } from '../shared/indexer.6f70ff10.mjs';
1
+ import { a as IndexerPlugin } from '../shared/indexer.b4be75f1.mjs';
2
2
  import { DataFinality, Cursor } from '@apibara/protocol';
3
3
  import { Database } from 'better-sqlite3';
4
4
  import 'hookable';
@@ -1,4 +1,4 @@
1
- import { a as IndexerPlugin } from '../shared/indexer.5a040ab3.js';
1
+ import { a as IndexerPlugin } from '../shared/indexer.a05207df.js';
2
2
  import { DataFinality, Cursor } from '@apibara/protocol';
3
3
  import { Database } from 'better-sqlite3';
4
4
  import 'hookable';
@@ -4,6 +4,7 @@ import { d as deserialize, s as serialize } from '../shared/indexer.0c0510df.mjs
4
4
  import 'node:fs/promises';
5
5
  import 'node:path';
6
6
  import 'klona/full';
7
+ import '@apibara/protocol';
7
8
  import 'consola';
8
9
  import 'hookable';
9
10
  import '../shared/indexer.e2bc22c0.mjs';
@@ -1,4 +1,4 @@
1
- import { a as IndexerPlugin } from '../shared/indexer.f97f8e70.cjs';
1
+ import { a as IndexerPlugin } from '../shared/indexer.3bc6abe3.cjs';
2
2
  import { ConsolaReporter, ConsolaInstance } from 'consola';
3
3
  export { ConsolaInstance, ConsolaReporter } from 'consola';
4
4
  import '@apibara/protocol';
@@ -1,4 +1,4 @@
1
- import { a as IndexerPlugin } from '../shared/indexer.6f70ff10.mjs';
1
+ import { a as IndexerPlugin } from '../shared/indexer.b4be75f1.mjs';
2
2
  import { ConsolaReporter, ConsolaInstance } from 'consola';
3
3
  export { ConsolaInstance, ConsolaReporter } from 'consola';
4
4
  import '@apibara/protocol';
@@ -1,4 +1,4 @@
1
- import { a as IndexerPlugin } from '../shared/indexer.5a040ab3.js';
1
+ import { a as IndexerPlugin } from '../shared/indexer.a05207df.js';
2
2
  import { ConsolaReporter, ConsolaInstance } from 'consola';
3
3
  export { ConsolaInstance, ConsolaReporter } from 'consola';
4
4
  import '@apibara/protocol';
@@ -4,6 +4,7 @@ const helper = require('../shared/indexer.219f58e0.cjs');
4
4
  require('node:fs/promises');
5
5
  require('node:path');
6
6
  require('klona/full');
7
+ require('@apibara/protocol');
7
8
  require('consola');
8
9
  require('hookable');
9
10
  require('node:assert');
@@ -22,6 +23,31 @@ var __publicField = (obj, key, value) => {
22
23
  __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
23
24
  return value;
24
25
  };
26
+ function inMemoryPersistence() {
27
+ return plugins_index.defineIndexerPlugin((indexer) => {
28
+ let lastCursor;
29
+ let lastFilter;
30
+ indexer.hooks.hook("connect:before", ({ request }) => {
31
+ if (lastCursor) {
32
+ request.startingCursor = lastCursor;
33
+ }
34
+ if (lastFilter) {
35
+ request.filter[1] = lastFilter;
36
+ }
37
+ });
38
+ indexer.hooks.hook("transaction:commit", ({ endCursor }) => {
39
+ if (endCursor) {
40
+ lastCursor = endCursor;
41
+ }
42
+ });
43
+ indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
44
+ if (request.filter[1]) {
45
+ lastCursor = endCursor;
46
+ lastFilter = request.filter[1];
47
+ }
48
+ });
49
+ });
50
+ }
25
51
  function sqlitePersistence({
26
52
  database
27
53
  }) {
@@ -181,4 +207,5 @@ const statements = {
181
207
  };
182
208
 
183
209
  exports.SqlitePersistence = SqlitePersistence;
210
+ exports.inMemoryPersistence = inMemoryPersistence;
184
211
  exports.sqlitePersistence = sqlitePersistence;
@@ -1,9 +1,10 @@
1
- import { a as IndexerPlugin } from '../shared/indexer.f97f8e70.cjs';
1
+ import { a as IndexerPlugin } from '../shared/indexer.3bc6abe3.cjs';
2
2
  import { Cursor } from '@apibara/protocol';
3
3
  import { Database } from 'better-sqlite3';
4
4
  import 'hookable';
5
5
  import '../shared/indexer.47b4546b.cjs';
6
6
 
7
+ declare function inMemoryPersistence<TFilter, TBlock, TTxnParams>(): IndexerPlugin<TFilter, TBlock, TTxnParams>;
7
8
  declare function sqlitePersistence<TFilter, TBlock, TTxnParams>({ database, }: {
8
9
  database: Database;
9
10
  }): IndexerPlugin<TFilter, TBlock, TTxnParams>;
@@ -47,4 +48,4 @@ type FilterRow = {
47
48
  to_block?: number;
48
49
  };
49
50
 
50
- export { type CheckpointRow, type FilterRow, SqlitePersistence, sqlitePersistence };
51
+ export { type CheckpointRow, type FilterRow, SqlitePersistence, inMemoryPersistence, sqlitePersistence };
@@ -1,9 +1,10 @@
1
- import { a as IndexerPlugin } from '../shared/indexer.6f70ff10.mjs';
1
+ import { a as IndexerPlugin } from '../shared/indexer.b4be75f1.mjs';
2
2
  import { Cursor } from '@apibara/protocol';
3
3
  import { Database } from 'better-sqlite3';
4
4
  import 'hookable';
5
5
  import '../shared/indexer.47b4546b.mjs';
6
6
 
7
+ declare function inMemoryPersistence<TFilter, TBlock, TTxnParams>(): IndexerPlugin<TFilter, TBlock, TTxnParams>;
7
8
  declare function sqlitePersistence<TFilter, TBlock, TTxnParams>({ database, }: {
8
9
  database: Database;
9
10
  }): IndexerPlugin<TFilter, TBlock, TTxnParams>;
@@ -47,4 +48,4 @@ type FilterRow = {
47
48
  to_block?: number;
48
49
  };
49
50
 
50
- export { type CheckpointRow, type FilterRow, SqlitePersistence, sqlitePersistence };
51
+ export { type CheckpointRow, type FilterRow, SqlitePersistence, inMemoryPersistence, sqlitePersistence };
@@ -1,9 +1,10 @@
1
- import { a as IndexerPlugin } from '../shared/indexer.5a040ab3.js';
1
+ import { a as IndexerPlugin } from '../shared/indexer.a05207df.js';
2
2
  import { Cursor } from '@apibara/protocol';
3
3
  import { Database } from 'better-sqlite3';
4
4
  import 'hookable';
5
5
  import '../shared/indexer.47b4546b.js';
6
6
 
7
+ declare function inMemoryPersistence<TFilter, TBlock, TTxnParams>(): IndexerPlugin<TFilter, TBlock, TTxnParams>;
7
8
  declare function sqlitePersistence<TFilter, TBlock, TTxnParams>({ database, }: {
8
9
  database: Database;
9
10
  }): IndexerPlugin<TFilter, TBlock, TTxnParams>;
@@ -47,4 +48,4 @@ type FilterRow = {
47
48
  to_block?: number;
48
49
  };
49
50
 
50
- export { type CheckpointRow, type FilterRow, SqlitePersistence, sqlitePersistence };
51
+ export { type CheckpointRow, type FilterRow, SqlitePersistence, inMemoryPersistence, sqlitePersistence };
@@ -2,6 +2,7 @@ import { d as deserialize, s as serialize } from '../shared/indexer.0c0510df.mjs
2
2
  import 'node:fs/promises';
3
3
  import 'node:path';
4
4
  import 'klona/full';
5
+ import '@apibara/protocol';
5
6
  import 'consola';
6
7
  import 'hookable';
7
8
  import 'node:assert';
@@ -20,6 +21,31 @@ var __publicField = (obj, key, value) => {
20
21
  __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
21
22
  return value;
22
23
  };
24
+ function inMemoryPersistence() {
25
+ return defineIndexerPlugin((indexer) => {
26
+ let lastCursor;
27
+ let lastFilter;
28
+ indexer.hooks.hook("connect:before", ({ request }) => {
29
+ if (lastCursor) {
30
+ request.startingCursor = lastCursor;
31
+ }
32
+ if (lastFilter) {
33
+ request.filter[1] = lastFilter;
34
+ }
35
+ });
36
+ indexer.hooks.hook("transaction:commit", ({ endCursor }) => {
37
+ if (endCursor) {
38
+ lastCursor = endCursor;
39
+ }
40
+ });
41
+ indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
42
+ if (request.filter[1]) {
43
+ lastCursor = endCursor;
44
+ lastFilter = request.filter[1];
45
+ }
46
+ });
47
+ });
48
+ }
23
49
  function sqlitePersistence({
24
50
  database
25
51
  }) {
@@ -178,4 +204,4 @@ const statements = {
178
204
  WHERE id = ?`
179
205
  };
180
206
 
181
- export { SqlitePersistence, sqlitePersistence };
207
+ export { SqlitePersistence, inMemoryPersistence, sqlitePersistence };
@@ -83,6 +83,15 @@ interface Indexer<TFilter, TBlock, TTxnParams> {
83
83
  hooks: Hookable<IndexerHooks<TFilter, TBlock>>;
84
84
  }
85
85
  declare function createIndexer<TFilter, TBlock, TTxnParams>({ streamConfig, ...options }: IndexerWithStreamConfig<TFilter, TBlock, TTxnParams>): Indexer<TFilter, TBlock, TTxnParams>;
86
- declare function run<TFilter, TBlock, TTxnParams>(client: Client<TFilter, TBlock>, indexer: Indexer<TFilter, TBlock, TTxnParams>): Promise<void>;
86
+ interface ReconnectOptions {
87
+ maxRetries?: number;
88
+ retryDelay?: number;
89
+ maxWait?: number;
90
+ }
91
+ declare function runWithReconnect<TFilter, TBlock, TTxnParams>(client: Client<TFilter, TBlock>, indexer: Indexer<TFilter, TBlock, TTxnParams>, options?: ReconnectOptions): Promise<void>;
92
+ interface RunOptions {
93
+ onConnect?: () => void | Promise<void>;
94
+ }
95
+ declare function run<TFilter, TBlock, TTxnParams>(client: Client<TFilter, TBlock>, indexer: Indexer<TFilter, TBlock, TTxnParams>, runOptions?: RunOptions): Promise<void>;
87
96
 
88
- export { type IndexerWithStreamConfig as I, type IndexerPlugin as a, type Indexer as b, type IndexerHooks as c, defineIndexerPlugin as d, type IndexerConfig as e, defineIndexer as f, createIndexer as g, run as r };
97
+ export { type IndexerWithStreamConfig as I, type ReconnectOptions as R, type IndexerPlugin as a, type Indexer as b, type IndexerHooks as c, defineIndexerPlugin as d, type IndexerConfig as e, defineIndexer as f, createIndexer as g, type RunOptions as h, run as i, runWithReconnect as r };
@@ -83,6 +83,15 @@ interface Indexer<TFilter, TBlock, TTxnParams> {
83
83
  hooks: Hookable<IndexerHooks<TFilter, TBlock>>;
84
84
  }
85
85
  declare function createIndexer<TFilter, TBlock, TTxnParams>({ streamConfig, ...options }: IndexerWithStreamConfig<TFilter, TBlock, TTxnParams>): Indexer<TFilter, TBlock, TTxnParams>;
86
- declare function run<TFilter, TBlock, TTxnParams>(client: Client<TFilter, TBlock>, indexer: Indexer<TFilter, TBlock, TTxnParams>): Promise<void>;
86
+ interface ReconnectOptions {
87
+ maxRetries?: number;
88
+ retryDelay?: number;
89
+ maxWait?: number;
90
+ }
91
+ declare function runWithReconnect<TFilter, TBlock, TTxnParams>(client: Client<TFilter, TBlock>, indexer: Indexer<TFilter, TBlock, TTxnParams>, options?: ReconnectOptions): Promise<void>;
92
+ interface RunOptions {
93
+ onConnect?: () => void | Promise<void>;
94
+ }
95
+ declare function run<TFilter, TBlock, TTxnParams>(client: Client<TFilter, TBlock>, indexer: Indexer<TFilter, TBlock, TTxnParams>, runOptions?: RunOptions): Promise<void>;
87
96
 
88
- export { type IndexerWithStreamConfig as I, type IndexerPlugin as a, type Indexer as b, type IndexerHooks as c, defineIndexerPlugin as d, type IndexerConfig as e, defineIndexer as f, createIndexer as g, run as r };
97
+ export { type IndexerWithStreamConfig as I, type ReconnectOptions as R, type IndexerPlugin as a, type Indexer as b, type IndexerHooks as c, defineIndexerPlugin as d, type IndexerConfig as e, defineIndexer as f, createIndexer as g, type RunOptions as h, run as i, runWithReconnect as r };
@@ -83,6 +83,15 @@ interface Indexer<TFilter, TBlock, TTxnParams> {
83
83
  hooks: Hookable<IndexerHooks<TFilter, TBlock>>;
84
84
  }
85
85
  declare function createIndexer<TFilter, TBlock, TTxnParams>({ streamConfig, ...options }: IndexerWithStreamConfig<TFilter, TBlock, TTxnParams>): Indexer<TFilter, TBlock, TTxnParams>;
86
- declare function run<TFilter, TBlock, TTxnParams>(client: Client<TFilter, TBlock>, indexer: Indexer<TFilter, TBlock, TTxnParams>): Promise<void>;
86
+ interface ReconnectOptions {
87
+ maxRetries?: number;
88
+ retryDelay?: number;
89
+ maxWait?: number;
90
+ }
91
+ declare function runWithReconnect<TFilter, TBlock, TTxnParams>(client: Client<TFilter, TBlock>, indexer: Indexer<TFilter, TBlock, TTxnParams>, options?: ReconnectOptions): Promise<void>;
92
+ interface RunOptions {
93
+ onConnect?: () => void | Promise<void>;
94
+ }
95
+ declare function run<TFilter, TBlock, TTxnParams>(client: Client<TFilter, TBlock>, indexer: Indexer<TFilter, TBlock, TTxnParams>, runOptions?: RunOptions): Promise<void>;
87
96
 
88
- export { type IndexerWithStreamConfig as I, type IndexerPlugin as a, type Indexer as b, type IndexerHooks as c, defineIndexerPlugin as d, type IndexerConfig as e, defineIndexer as f, createIndexer as g, run as r };
97
+ export { type IndexerWithStreamConfig as I, type ReconnectOptions as R, type IndexerPlugin as a, type Indexer as b, type IndexerHooks as c, defineIndexerPlugin as d, type IndexerConfig as e, defineIndexer as f, createIndexer as g, type RunOptions as h, run as i, runWithReconnect as r };
@@ -1,4 +1,4 @@
1
- import { I as IndexerWithStreamConfig } from '../shared/indexer.f97f8e70.cjs';
1
+ import { I as IndexerWithStreamConfig } from '../shared/indexer.3bc6abe3.cjs';
2
2
  import '@apibara/protocol';
3
3
  import 'hookable';
4
4
  import '../shared/indexer.47b4546b.cjs';
@@ -1,4 +1,4 @@
1
- import { I as IndexerWithStreamConfig } from '../shared/indexer.6f70ff10.mjs';
1
+ import { I as IndexerWithStreamConfig } from '../shared/indexer.b4be75f1.mjs';
2
2
  import '@apibara/protocol';
3
3
  import 'hookable';
4
4
  import '../shared/indexer.47b4546b.mjs';
@@ -1,4 +1,4 @@
1
- import { I as IndexerWithStreamConfig } from '../shared/indexer.5a040ab3.js';
1
+ import { I as IndexerWithStreamConfig } from '../shared/indexer.a05207df.js';
2
2
  import '@apibara/protocol';
3
3
  import 'hookable';
4
4
  import '../shared/indexer.47b4546b.js';
@@ -8,6 +8,7 @@ const index = require('../index.cjs');
8
8
  const assert = require('node:assert');
9
9
  const fs$1 = require('node:fs');
10
10
  const testing = require('@apibara/protocol/testing');
11
+ require('@apibara/protocol');
11
12
  require('consola');
12
13
  require('hookable');
13
14
  require('../shared/indexer.077335f3.cjs');
@@ -1,5 +1,5 @@
1
1
  import { Cursor, StreamDataResponse, Client } from '@apibara/protocol';
2
- import { b as Indexer } from '../shared/indexer.f97f8e70.cjs';
2
+ import { b as Indexer } from '../shared/indexer.3bc6abe3.cjs';
3
3
  import 'hookable';
4
4
  import '../shared/indexer.47b4546b.cjs';
5
5
 
@@ -1,5 +1,5 @@
1
1
  import { Cursor, StreamDataResponse, Client } from '@apibara/protocol';
2
- import { b as Indexer } from '../shared/indexer.6f70ff10.mjs';
2
+ import { b as Indexer } from '../shared/indexer.b4be75f1.mjs';
3
3
  import 'hookable';
4
4
  import '../shared/indexer.47b4546b.mjs';
5
5
 
@@ -1,5 +1,5 @@
1
1
  import { Cursor, StreamDataResponse, Client } from '@apibara/protocol';
2
- import { b as Indexer } from '../shared/indexer.5a040ab3.js';
2
+ import { b as Indexer } from '../shared/indexer.a05207df.js';
3
3
  import 'hookable';
4
4
  import '../shared/indexer.47b4546b.js';
5
5
 
@@ -7,6 +7,7 @@ import { run } from '../index.mjs';
7
7
  import assert from 'node:assert';
8
8
  import fs$1 from 'node:fs';
9
9
  import { MockClient } from '@apibara/protocol/testing';
10
+ import '@apibara/protocol';
10
11
  import 'consola';
11
12
  import 'hookable';
12
13
  import '../shared/indexer.a55ad619.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apibara/indexer",
3
- "version": "2.0.0-beta.20",
3
+ "version": "2.0.0-beta.22",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -92,7 +92,7 @@
92
92
  "vitest": "^1.6.0"
93
93
  },
94
94
  "dependencies": {
95
- "@apibara/protocol": "2.0.0-beta.20",
95
+ "@apibara/protocol": "2.0.0-beta.22",
96
96
  "@opentelemetry/api": "^1.9.0",
97
97
  "ci-info": "^4.1.0",
98
98
  "consola": "^3.2.3",
package/src/indexer.ts CHANGED
@@ -1,15 +1,17 @@
1
- import type {
2
- Client,
3
- Cursor,
4
- DataFinality,
5
- Finalize,
6
- Heartbeat,
7
- Invalidate,
8
- StreamConfig,
9
- StreamDataOptions,
10
- StreamDataRequest,
11
- StreamDataResponse,
12
- SystemMessage,
1
+ import {
2
+ type Client,
3
+ ClientError,
4
+ type Cursor,
5
+ type DataFinality,
6
+ type Finalize,
7
+ type Heartbeat,
8
+ type Invalidate,
9
+ Status,
10
+ type StreamConfig,
11
+ type StreamDataOptions,
12
+ type StreamDataRequest,
13
+ type StreamDataResponse,
14
+ type SystemMessage,
13
15
  } from "@apibara/protocol";
14
16
  import consola from "consola";
15
17
  import {
@@ -149,9 +151,71 @@ export function createIndexer<TFilter, TBlock, TTxnParams>({
149
151
  return indexer;
150
152
  }
151
153
 
154
+ export interface ReconnectOptions {
155
+ maxRetries?: number;
156
+ retryDelay?: number;
157
+ maxWait?: number;
158
+ }
159
+
160
+ export async function runWithReconnect<TFilter, TBlock, TTxnParams>(
161
+ client: Client<TFilter, TBlock>,
162
+ indexer: Indexer<TFilter, TBlock, TTxnParams>,
163
+ options: ReconnectOptions = {},
164
+ ) {
165
+ let retryCount = 0;
166
+
167
+ const maxRetries = options.maxRetries ?? 10;
168
+ const retryDelay = options.retryDelay ?? 1_000;
169
+ const maxWait = options.maxWait ?? 30_000;
170
+
171
+ const runOptions: RunOptions = {
172
+ onConnect() {
173
+ retryCount = 0;
174
+ },
175
+ };
176
+
177
+ while (true) {
178
+ try {
179
+ await run(client, indexer, runOptions);
180
+ return;
181
+ } catch (error) {
182
+ // Only reconnect on internal/server errors.
183
+ // All other errors should be rethrown.
184
+
185
+ retryCount++;
186
+
187
+ if (error instanceof ClientError) {
188
+ if (error.code === Status.INTERNAL) {
189
+ if (retryCount < maxRetries) {
190
+ consola.error(
191
+ "Internal server error, reconnecting...",
192
+ error.message,
193
+ );
194
+
195
+ // Add jitter to the retry delay to avoid all clients retrying at the same time.
196
+ const delay = Math.random() * (retryDelay * 0.2) + retryDelay;
197
+ await new Promise((resolve) =>
198
+ setTimeout(resolve, Math.min(retryCount * delay, maxWait)),
199
+ );
200
+
201
+ continue;
202
+ }
203
+ }
204
+ }
205
+
206
+ throw error;
207
+ }
208
+ }
209
+ }
210
+
211
+ export interface RunOptions {
212
+ onConnect?: () => void | Promise<void>;
213
+ }
214
+
152
215
  export async function run<TFilter, TBlock, TTxnParams>(
153
216
  client: Client<TFilter, TBlock>,
154
217
  indexer: Indexer<TFilter, TBlock, TTxnParams>,
218
+ runOptions: RunOptions = {},
155
219
  ) {
156
220
  await indexerAsyncContext.callAsync({}, async () => {
157
221
  const context = useIndexerContext<TTxnParams>();
@@ -195,6 +259,8 @@ export async function run<TFilter, TBlock, TTxnParams>(
195
259
 
196
260
  await indexer.hooks.callHook("connect:after");
197
261
 
262
+ let onConnectCalled = false;
263
+
198
264
  while (true) {
199
265
  const { value: message, done } = await stream.next();
200
266
 
@@ -202,6 +268,13 @@ export async function run<TFilter, TBlock, TTxnParams>(
202
268
  break;
203
269
  }
204
270
 
271
+ if (!onConnectCalled) {
272
+ onConnectCalled = true;
273
+ if (runOptions.onConnect) {
274
+ await runOptions.onConnect();
275
+ }
276
+ }
277
+
205
278
  await indexer.hooks.callHook("message", { message });
206
279
 
207
280
  switch (message._tag) {
@@ -3,6 +3,36 @@ import type { Database as SqliteDatabase, Statement } from "better-sqlite3";
3
3
  import { deserialize, serialize } from "../vcr";
4
4
  import { defineIndexerPlugin } from "./config";
5
5
 
6
+ export function inMemoryPersistence<TFilter, TBlock, TTxnParams>() {
7
+ return defineIndexerPlugin<TFilter, TBlock, TTxnParams>((indexer) => {
8
+ let lastCursor: Cursor | undefined;
9
+ let lastFilter: TFilter | undefined;
10
+
11
+ indexer.hooks.hook("connect:before", ({ request }) => {
12
+ if (lastCursor) {
13
+ request.startingCursor = lastCursor;
14
+ }
15
+
16
+ if (lastFilter) {
17
+ request.filter[1] = lastFilter;
18
+ }
19
+ });
20
+
21
+ indexer.hooks.hook("transaction:commit", ({ endCursor }) => {
22
+ if (endCursor) {
23
+ lastCursor = endCursor;
24
+ }
25
+ });
26
+
27
+ indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
28
+ if (request.filter[1]) {
29
+ lastCursor = endCursor;
30
+ lastFilter = request.filter[1];
31
+ }
32
+ });
33
+ });
34
+ }
35
+
6
36
  export function sqlitePersistence<TFilter, TBlock, TTxnParams>({
7
37
  database,
8
38
  }: { database: SqliteDatabase }) {