@midnightntwrk/wallet-sdk-indexer-client 1.2.3
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/README.md +86 -0
- package/dist/effect/Backpressure.d.ts +97 -0
- package/dist/effect/Backpressure.js +146 -0
- package/dist/effect/ConnectionHelper.d.ts +4 -0
- package/dist/effect/ConnectionHelper.js +41 -0
- package/dist/effect/HttpQueryClient.d.ts +4 -0
- package/dist/effect/HttpQueryClient.js +50 -0
- package/dist/effect/Query.d.ts +39 -0
- package/dist/effect/Query.js +47 -0
- package/dist/effect/QueryClient.d.ts +15 -0
- package/dist/effect/QueryClient.js +15 -0
- package/dist/effect/QueryRunner.d.ts +11 -0
- package/dist/effect/QueryRunner.js +23 -0
- package/dist/effect/Subscription.d.ts +25 -0
- package/dist/effect/Subscription.js +56 -0
- package/dist/effect/SubscriptionClient.d.ts +23 -0
- package/dist/effect/SubscriptionClient.js +15 -0
- package/dist/effect/WsSubscriptionClient.d.ts +4 -0
- package/dist/effect/WsSubscriptionClient.js +89 -0
- package/dist/effect/index.d.ts +8 -0
- package/dist/effect/index.js +20 -0
- package/dist/effect/test/backpressure.test.d.ts +1 -0
- package/dist/effect/test/backpressure.test.js +146 -0
- package/dist/effect/test/connectionHelper.test.d.ts +1 -0
- package/dist/effect/test/connectionHelper.test.js +34 -0
- package/dist/effect/test/httpQueryClient.spied.test.d.ts +1 -0
- package/dist/effect/test/httpQueryClient.spied.test.js +37 -0
- package/dist/effect/test/httpQueryClient.test.d.ts +1 -0
- package/dist/effect/test/httpQueryClient.test.js +32 -0
- package/dist/effect/test/queryRunner.spied.test.d.ts +1 -0
- package/dist/effect/test/queryRunner.spied.test.js +46 -0
- package/dist/effect/test/wsSubscriptionClient.spied.test.d.ts +1 -0
- package/dist/effect/test/wsSubscriptionClient.spied.test.js +37 -0
- package/dist/effect/test/wsSubscriptionClient.test.d.ts +1 -0
- package/dist/effect/test/wsSubscriptionClient.test.js +32 -0
- package/dist/graphql/generated/fragment-masking.d.ts +19 -0
- package/dist/graphql/generated/fragment-masking.js +16 -0
- package/dist/graphql/generated/gql.d.ts +61 -0
- package/dist/graphql/generated/gql.js +17 -0
- package/dist/graphql/generated/graphql.d.ts +1021 -0
- package/dist/graphql/generated/graphql.js +700 -0
- package/dist/graphql/generated/index.d.ts +2 -0
- package/dist/graphql/generated/index.js +2 -0
- package/dist/graphql/queries/BlockHash.d.ts +6 -0
- package/dist/graphql/queries/BlockHash.js +24 -0
- package/dist/graphql/queries/Connect.d.ts +6 -0
- package/dist/graphql/queries/Connect.js +19 -0
- package/dist/graphql/queries/Disconnect.d.ts +6 -0
- package/dist/graphql/queries/Disconnect.js +19 -0
- package/dist/graphql/queries/FetchTermsAndConditions.d.ts +6 -0
- package/dist/graphql/queries/FetchTermsAndConditions.js +26 -0
- package/dist/graphql/queries/TransactionHistoryDetail.d.ts +6 -0
- package/dist/graphql/queries/TransactionHistoryDetail.js +30 -0
- package/dist/graphql/queries/TransactionStatus.d.ts +6 -0
- package/dist/graphql/queries/TransactionStatus.js +34 -0
- package/dist/graphql/queries/index.d.ts +6 -0
- package/dist/graphql/queries/index.js +18 -0
- package/dist/graphql/queries/test/BlockHash.test.d.ts +1 -0
- package/dist/graphql/queries/test/BlockHash.test.js +84 -0
- package/dist/graphql/subscriptions/DustLedgerEvents.d.ts +6 -0
- package/dist/graphql/subscriptions/DustLedgerEvents.js +24 -0
- package/dist/graphql/subscriptions/ShieldedTransactions.d.ts +8 -0
- package/dist/graphql/subscriptions/ShieldedTransactions.js +54 -0
- package/dist/graphql/subscriptions/UnshieldedTransactions.d.ts +8 -0
- package/dist/graphql/subscriptions/UnshieldedTransactions.js +68 -0
- package/dist/graphql/subscriptions/ZswapEvents.d.ts +6 -0
- package/dist/graphql/subscriptions/ZswapEvents.js +24 -0
- package/dist/graphql/subscriptions/index.d.ts +4 -0
- package/dist/graphql/subscriptions/index.js +16 -0
- package/dist/graphql/subscriptions/test/ShieldedTransactions.test.d.ts +1 -0
- package/dist/graphql/subscriptions/test/ShieldedTransactions.test.js +55 -0
- package/dist/graphql/subscriptions/test/UnshieldedTransactions.test.d.ts +1 -0
- package/dist/graphql/subscriptions/test/UnshieldedTransactions.test.js +52 -0
- package/dist/graphql/subscriptions/test/ZswapEvents.test.d.ts +1 -0
- package/dist/graphql/subscriptions/test/ZswapEvents.test.js +50 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +15 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# @midnightntwrk/wallet-sdk-indexer-client
|
|
2
|
+
|
|
3
|
+
Client for communicating with the Midnight indexer service.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @midnightntwrk/wallet-sdk-indexer-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
This package provides a GraphQL-based client for interacting with the Midnight indexer service. The indexer aggregates
|
|
14
|
+
blockchain data and provides efficient access to:
|
|
15
|
+
|
|
16
|
+
- **Subscriptions** for real-time event streaming:
|
|
17
|
+
- Shielded transactions (Zswap)
|
|
18
|
+
- Unshielded transactions
|
|
19
|
+
- Zswap events
|
|
20
|
+
- Dust ledger events
|
|
21
|
+
|
|
22
|
+
- **Queries** for fetching blockchain data:
|
|
23
|
+
- Block information (height, hash, timestamp)
|
|
24
|
+
- Ledger parameters (including cost model)
|
|
25
|
+
- Connection management
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Subscriptions
|
|
30
|
+
|
|
31
|
+
Subscribe to real-time events from the indexer:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import {
|
|
35
|
+
ShieldedTransactions,
|
|
36
|
+
UnshieldedTransactions,
|
|
37
|
+
ZswapEvents,
|
|
38
|
+
DustLedgerEvents,
|
|
39
|
+
} from '@midnightntwrk/wallet-sdk-indexer-client';
|
|
40
|
+
|
|
41
|
+
// Subscribe to shielded transaction events
|
|
42
|
+
// Subscribe to unshielded transaction events
|
|
43
|
+
// Subscribe to Zswap events
|
|
44
|
+
// Subscribe to dust ledger events
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Queries
|
|
48
|
+
|
|
49
|
+
Query blockchain data:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { BlockHash } from '@midnightntwrk/wallet-sdk-indexer-client';
|
|
53
|
+
|
|
54
|
+
// Query block information including:
|
|
55
|
+
// - Block height
|
|
56
|
+
// - Block hash
|
|
57
|
+
// - Ledger parameters (cost model, etc.)
|
|
58
|
+
// - Timestamp
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Exports
|
|
62
|
+
|
|
63
|
+
### Default Export
|
|
64
|
+
|
|
65
|
+
- GraphQL queries: `Connect`, `Disconnect`, `BlockHash`
|
|
66
|
+
- GraphQL subscriptions: `ShieldedTransactions`, `UnshieldedTransactions`, `ZswapEvents`, `DustLedgerEvents`
|
|
67
|
+
- Generated GraphQL types
|
|
68
|
+
|
|
69
|
+
### Effect Submodule (`/effect`)
|
|
70
|
+
|
|
71
|
+
Effect.ts-based implementation for functional programming patterns:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import {
|
|
75
|
+
Query,
|
|
76
|
+
Subscription,
|
|
77
|
+
QueryClient,
|
|
78
|
+
SubscriptionClient,
|
|
79
|
+
HttpQueryClient,
|
|
80
|
+
WsSubscriptionClient,
|
|
81
|
+
} from '@midnightntwrk/wallet-sdk-indexer-client/effect';
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
Apache-2.0
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Stream } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Options controlling the bounded backpressure behavior.
|
|
4
|
+
*
|
|
5
|
+
* The implementation caps the number of in-flight (emitted but not yet consumed downstream) items. Once
|
|
6
|
+
* {@link bufferSize} is reached the underlying source is disposed; when the consumer drains back down to
|
|
7
|
+
* {@link resumeThreshold} a fresh subscription is opened with `variables(cursor)` for the running cursor.
|
|
8
|
+
*
|
|
9
|
+
* Emitted items are guaranteed strictly monotonic by {@link key}. Dedup uses a single exclusive watermark: any item with
|
|
10
|
+
* a key `<= watermark` is dropped, and each emitted key advances the watermark. The watermark seeds from {@link from},
|
|
11
|
+
* so the item _at_ `from` is treated as already-seen and dropped. This is what makes resume safe over an inclusive
|
|
12
|
+
* cursor: the boundary item a resume re-delivers has a key equal to the watermark and is dropped — without it, the
|
|
13
|
+
* first item after a resume would duplicate the last one emitted before the pause. A caller that needs the item at its
|
|
14
|
+
* logical cursor emitted (e.g. to let an already-caught-up consumer observe its tip) passes `from` one below that
|
|
15
|
+
* cursor.
|
|
16
|
+
*
|
|
17
|
+
* The cursor is the single source of truth for both initial open and resume — no separate "initial variables" field;
|
|
18
|
+
* `variables(from)` opens the first subscription, `variables(key(lastItem))` opens every subsequent one.
|
|
19
|
+
*
|
|
20
|
+
* No items above the watermark are dropped — the producer is paused, never thrown away.
|
|
21
|
+
*
|
|
22
|
+
* **Totality contract:** {@link key} and {@link variables} MUST be total — they must not throw for any input of their
|
|
23
|
+
* declared types. The wrapper does not catch exceptions from these closures: a throw from `key` propagates back through
|
|
24
|
+
* the source's sync callback (undefined behavior depending on the source's internals), and a throw from `variables`
|
|
25
|
+
* surfaces as an unhandled defect on the resulting `Stream` rather than a typed `E`. The signatures `(cursor: bigint)
|
|
26
|
+
* => V` and `(item: Item) => bigint` are the contract; honor them.
|
|
27
|
+
*/
|
|
28
|
+
export interface BackpressureOptions<Item, V> {
|
|
29
|
+
/** Pause the underlying subscription once in-flight reaches this count. */
|
|
30
|
+
readonly bufferSize: number;
|
|
31
|
+
/**
|
|
32
|
+
* Resume once in-flight drains back to this count. Must be strictly less than `bufferSize` to give the consumer real
|
|
33
|
+
* hysteresis.
|
|
34
|
+
*/
|
|
35
|
+
readonly resumeThreshold: number;
|
|
36
|
+
/** Initial cursor (bigint watermark). The first subscription opens with `variables(from)`. */
|
|
37
|
+
readonly from: bigint;
|
|
38
|
+
/** Derives subscription variables from a cursor. Used for the initial open and on every resume. */
|
|
39
|
+
readonly variables: (cursor: bigint) => V;
|
|
40
|
+
/** Monotonic key extractor — items with a key not strictly greater than the running watermark are dropped. */
|
|
41
|
+
readonly key: (result: Item) => bigint;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A push-based source. Each call opens a fresh session and returns a dispose handle. Items, errors and completion flow
|
|
45
|
+
* through the supplied callbacks.
|
|
46
|
+
*
|
|
47
|
+
* The source MUST stop delivering callbacks after `dispose()` is invoked. The caller pauses by disposing and resumes by
|
|
48
|
+
* calling the factory again with new variables; the source has no other backpressure signalling.
|
|
49
|
+
*/
|
|
50
|
+
export type Source<Item, V, E> = (params: {
|
|
51
|
+
readonly variables: V;
|
|
52
|
+
readonly onItem: (item: Item) => void;
|
|
53
|
+
readonly onError: (error: E) => void;
|
|
54
|
+
readonly onComplete: () => void;
|
|
55
|
+
}) => () => void;
|
|
56
|
+
/** @internal — exported only for unit tests; not part of the public API. */
|
|
57
|
+
export type BPState = {
|
|
58
|
+
readonly paused: boolean;
|
|
59
|
+
readonly inFlight: number;
|
|
60
|
+
readonly lastKey: bigint;
|
|
61
|
+
readonly generation: number;
|
|
62
|
+
readonly terminal: boolean;
|
|
63
|
+
};
|
|
64
|
+
/** @internal */
|
|
65
|
+
export type ItemDecision = {
|
|
66
|
+
readonly emit: boolean;
|
|
67
|
+
readonly pause: boolean;
|
|
68
|
+
};
|
|
69
|
+
/** @internal */
|
|
70
|
+
export declare const initialBPState: (from: bigint) => BPState;
|
|
71
|
+
/**
|
|
72
|
+
* @internal Decide what to do with an item arriving on `onItem`.
|
|
73
|
+
*
|
|
74
|
+
* The watermark is exclusive throughout: an item is dropped iff its key is `<= lastKey`, and each emitted key advances
|
|
75
|
+
* the watermark. The watermark seeds from `from`, so the item at `from` itself is treated as already-seen and dropped —
|
|
76
|
+
* a caller that wants the item at its logical cursor emitted seeds `from` one below it. The same exclusive rule drops
|
|
77
|
+
* the boundary item an inclusive-cursor resume re-delivers (its key equals `lastKey`).
|
|
78
|
+
*/
|
|
79
|
+
export declare const decideItem: (s: BPState, k: bigint, myGen: number, bufferSize: number) => readonly [ItemDecision, BPState];
|
|
80
|
+
/** @internal Decide whether an error callback should surface (false for stale/terminal). */
|
|
81
|
+
export declare const decideTerminate: (s: BPState, myGen: number) => readonly [boolean, BPState];
|
|
82
|
+
/** @internal Decide whether a completion is consumer-visible end-of-stream or dispose-induced. */
|
|
83
|
+
export declare const decideComplete: (s: BPState, myGen: number) => readonly [boolean, BPState];
|
|
84
|
+
/**
|
|
85
|
+
* @internal Decide whether to resume the source after a downstream consume. Paused implies we've emitted at least
|
|
86
|
+
* bufferSize items, so lastKey is meaningful. Returns true when the caller should open a new session — the caller
|
|
87
|
+
* derives the resume variables from `state.lastKey` itself.
|
|
88
|
+
*/
|
|
89
|
+
export declare const decideResume: (s: BPState, resumeThreshold: number) => readonly [boolean, BPState];
|
|
90
|
+
/**
|
|
91
|
+
* Wrap a push-based {@link Source} with bounded backpressure. The source is paused (disposed) when the in-flight count
|
|
92
|
+
* reaches `bufferSize` and resumed (re-opened with `variables(lastKey)`) once the consumer drains back to
|
|
93
|
+
* `resumeThreshold`.
|
|
94
|
+
*
|
|
95
|
+
* See {@link BackpressureOptions} for the monotonic-key dedup invariant.
|
|
96
|
+
*/
|
|
97
|
+
export declare const withBackpressure: <Item, V, E>(source: Source<Item, V, E>, options: BackpressureOptions<Item, V>) => Stream.Stream<Item, E>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// This file is part of MIDNIGHT-WALLET-SDK.
|
|
2
|
+
// Copyright (C) Midnight Foundation
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// You may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
//
|
|
14
|
+
// Bounded backpressure over a push-only source whose only knob is "dispose and
|
|
15
|
+
// re-open from a cursor". The classic shape for graphql-ws / SSE-style feeds
|
|
16
|
+
// that don't support pull or pause signalling.
|
|
17
|
+
//
|
|
18
|
+
// Composition:
|
|
19
|
+
// - Pure state machine (decideItem/decideTerminate/decideComplete/decideResume)
|
|
20
|
+
// contains all transition rules and is unit-testable in isolation.
|
|
21
|
+
// - `withBackpressure` wires the state machine to a {@link Source}, an
|
|
22
|
+
// internal Stream.asyncPush queue, and a Stream.tap drain detector.
|
|
23
|
+
// - Source authors translate their callbacks to the {@link Source} contract
|
|
24
|
+
// once; backpressure is independent of the underlying transport.
|
|
25
|
+
import { Effect, Stream, Ref } from 'effect';
|
|
26
|
+
/** @internal */
|
|
27
|
+
export const initialBPState = (from) => ({
|
|
28
|
+
paused: false,
|
|
29
|
+
inFlight: 0,
|
|
30
|
+
lastKey: from,
|
|
31
|
+
generation: 0,
|
|
32
|
+
terminal: false,
|
|
33
|
+
});
|
|
34
|
+
/**
|
|
35
|
+
* @internal Decide what to do with an item arriving on `onItem`.
|
|
36
|
+
*
|
|
37
|
+
* The watermark is exclusive throughout: an item is dropped iff its key is `<= lastKey`, and each emitted key advances
|
|
38
|
+
* the watermark. The watermark seeds from `from`, so the item at `from` itself is treated as already-seen and dropped —
|
|
39
|
+
* a caller that wants the item at its logical cursor emitted seeds `from` one below it. The same exclusive rule drops
|
|
40
|
+
* the boundary item an inclusive-cursor resume re-delivers (its key equals `lastKey`).
|
|
41
|
+
*/
|
|
42
|
+
export const decideItem = (s, k, myGen, bufferSize) => {
|
|
43
|
+
if (s.terminal || s.generation !== myGen)
|
|
44
|
+
return [{ emit: false, pause: false }, s];
|
|
45
|
+
if (k <= s.lastKey)
|
|
46
|
+
return [{ emit: false, pause: false }, s];
|
|
47
|
+
const inFlight = s.inFlight + 1;
|
|
48
|
+
const pause = inFlight >= bufferSize && !s.paused;
|
|
49
|
+
return [
|
|
50
|
+
{ emit: true, pause },
|
|
51
|
+
{ ...s, inFlight, lastKey: k, paused: s.paused || pause },
|
|
52
|
+
];
|
|
53
|
+
};
|
|
54
|
+
/** @internal Decide whether an error callback should surface (false for stale/terminal). */
|
|
55
|
+
export const decideTerminate = (s, myGen) => s.terminal || s.generation !== myGen ? [false, s] : [true, { ...s, terminal: true }];
|
|
56
|
+
/** @internal Decide whether a completion is consumer-visible end-of-stream or dispose-induced. */
|
|
57
|
+
export const decideComplete = (s, myGen) => {
|
|
58
|
+
if (s.paused || s.terminal || s.generation !== myGen)
|
|
59
|
+
return [false, s];
|
|
60
|
+
return [true, { ...s, terminal: true }];
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* @internal Decide whether to resume the source after a downstream consume. Paused implies we've emitted at least
|
|
64
|
+
* bufferSize items, so lastKey is meaningful. Returns true when the caller should open a new session — the caller
|
|
65
|
+
* derives the resume variables from `state.lastKey` itself.
|
|
66
|
+
*/
|
|
67
|
+
export const decideResume = (s, resumeThreshold) => {
|
|
68
|
+
if (s.terminal)
|
|
69
|
+
return [false, s];
|
|
70
|
+
const inFlight = Math.max(0, s.inFlight - 1);
|
|
71
|
+
if (s.paused && inFlight <= resumeThreshold) {
|
|
72
|
+
return [true, { ...s, inFlight, paused: false, generation: s.generation + 1 }];
|
|
73
|
+
}
|
|
74
|
+
return [false, { ...s, inFlight }];
|
|
75
|
+
};
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Stream wrapper
|
|
78
|
+
// ============================================================================
|
|
79
|
+
/**
|
|
80
|
+
* Wrap a push-based {@link Source} with bounded backpressure. The source is paused (disposed) when the in-flight count
|
|
81
|
+
* reaches `bufferSize` and resumed (re-opened with `variables(lastKey)`) once the consumer drains back to
|
|
82
|
+
* `resumeThreshold`.
|
|
83
|
+
*
|
|
84
|
+
* See {@link BackpressureOptions} for the monotonic-key dedup invariant.
|
|
85
|
+
*/
|
|
86
|
+
export const withBackpressure = (source, options) => {
|
|
87
|
+
const { bufferSize, resumeThreshold, key, from, variables } = options;
|
|
88
|
+
// Stream.unwrap re-runs the factory per subscription, so each consumer of
|
|
89
|
+
// the returned Stream gets its own Refs and its own source session.
|
|
90
|
+
return Stream.unwrap(Effect.gen(function* () {
|
|
91
|
+
// Three refs cover the inherently mutable bridge between sync source
|
|
92
|
+
// callbacks and Effect: the state machine, the current dispose handle,
|
|
93
|
+
// and the asyncPush emit handle (only bound while a consumer is
|
|
94
|
+
// attached).
|
|
95
|
+
const stateRef = yield* Ref.make(initialBPState(from));
|
|
96
|
+
const disposerRef = yield* Ref.make(null);
|
|
97
|
+
const emitRef = yield* Ref.make(null);
|
|
98
|
+
// Source callbacks are sync JS. Ref ops are sync internally, so runSync
|
|
99
|
+
// is safe here. Confined to these two helpers for auditability.
|
|
100
|
+
const modify = (f) => Effect.runSync(Ref.modify(stateRef, f));
|
|
101
|
+
const takeDisposer = () => Effect.runSync(Ref.getAndSet(disposerRef, null));
|
|
102
|
+
const openSession = () => Effect.gen(function* () {
|
|
103
|
+
const state = yield* Ref.get(stateRef);
|
|
104
|
+
if (state.terminal)
|
|
105
|
+
return;
|
|
106
|
+
const emit = yield* Ref.get(emitRef);
|
|
107
|
+
if (emit === null)
|
|
108
|
+
return;
|
|
109
|
+
const myGen = state.generation;
|
|
110
|
+
const dispose = source({
|
|
111
|
+
variables: variables(state.lastKey),
|
|
112
|
+
onItem: (item) => {
|
|
113
|
+
const action = modify((s) => decideItem(s, key(item), myGen, bufferSize));
|
|
114
|
+
if (!action.emit)
|
|
115
|
+
return;
|
|
116
|
+
emit.single(item);
|
|
117
|
+
if (action.pause)
|
|
118
|
+
takeDisposer()?.();
|
|
119
|
+
},
|
|
120
|
+
onError: (error) => {
|
|
121
|
+
if (modify((s) => decideTerminate(s, myGen)))
|
|
122
|
+
emit.fail(error);
|
|
123
|
+
},
|
|
124
|
+
onComplete: () => {
|
|
125
|
+
if (modify((s) => decideComplete(s, myGen)))
|
|
126
|
+
emit.end();
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
yield* Ref.set(disposerRef, dispose);
|
|
130
|
+
});
|
|
131
|
+
return Stream.asyncPush((emit) => Effect.acquireRelease(Effect.gen(function* () {
|
|
132
|
+
yield* Ref.set(emitRef, emit);
|
|
133
|
+
yield* openSession();
|
|
134
|
+
}), () => Effect.gen(function* () {
|
|
135
|
+
yield* Ref.update(stateRef, (s) => ({ ...s, terminal: true }));
|
|
136
|
+
const d = yield* Ref.getAndSet(disposerRef, null);
|
|
137
|
+
if (d !== null)
|
|
138
|
+
yield* Effect.sync(() => d());
|
|
139
|
+
yield* Ref.set(emitRef, null);
|
|
140
|
+
})), { bufferSize: 'unbounded' }).pipe(Stream.tap(() => Effect.gen(function* () {
|
|
141
|
+
const shouldResume = yield* Ref.modify(stateRef, (s) => decideResume(s, resumeThreshold));
|
|
142
|
+
if (shouldResume)
|
|
143
|
+
yield* openSession();
|
|
144
|
+
})));
|
|
145
|
+
}));
|
|
146
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import * as Either from 'effect/Either';
|
|
2
|
+
import { type URLError } from '@midnightntwrk/wallet-sdk-utilities/networking';
|
|
3
|
+
export declare const deriveWebSocketUrl: (url: URL | string) => Either.Either<string, URLError>;
|
|
4
|
+
export declare const createWebSocketUrl: (httpUrl: URL | string, wsUrl?: string) => Either.Either<string, URLError>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// This file is part of MIDNIGHT-WALLET-SDK.
|
|
2
|
+
// Copyright (C) Midnight Foundation
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// You may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
import * as Either from 'effect/Either';
|
|
14
|
+
import { FailedToDeriveWebSocketUrlError } from '@midnightntwrk/wallet-sdk-utilities/networking';
|
|
15
|
+
export const deriveWebSocketUrl = (url) => {
|
|
16
|
+
const httpUrl = typeof url === 'string' ? new URL(url) : url;
|
|
17
|
+
try {
|
|
18
|
+
const wsUrl = new URL(httpUrl);
|
|
19
|
+
// Convert protocol
|
|
20
|
+
wsUrl.protocol = httpUrl.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
21
|
+
// Ensure pathname ends with '/ws'
|
|
22
|
+
if (!wsUrl.pathname.endsWith('/')) {
|
|
23
|
+
wsUrl.pathname += '/';
|
|
24
|
+
}
|
|
25
|
+
wsUrl.pathname += 'ws';
|
|
26
|
+
return Either.right(wsUrl.toString());
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
30
|
+
return Either.left(new FailedToDeriveWebSocketUrlError({
|
|
31
|
+
message: `Failed to derive WebSocket URL from ${httpUrl.toString()}`,
|
|
32
|
+
cause: error,
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
export const createWebSocketUrl = (httpUrl, wsUrl) => {
|
|
37
|
+
if (wsUrl) {
|
|
38
|
+
return Either.right(wsUrl);
|
|
39
|
+
}
|
|
40
|
+
return deriveWebSocketUrl(httpUrl);
|
|
41
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Layer, type Scope } from 'effect';
|
|
2
|
+
import { QueryClient } from './QueryClient.js';
|
|
3
|
+
import { type InvalidProtocolSchemeError } from '@midnightntwrk/wallet-sdk-utilities/networking';
|
|
4
|
+
export declare const layer: (config: QueryClient.ServerConfig) => Layer.Layer<QueryClient, InvalidProtocolSchemeError, Scope.Scope>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// This file is part of MIDNIGHT-WALLET-SDK.
|
|
2
|
+
// Copyright (C) Midnight Foundation
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// You may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
import { Effect, Layer } from 'effect';
|
|
14
|
+
import { print } from 'graphql';
|
|
15
|
+
import { createClient } from 'graphql-http';
|
|
16
|
+
import { QueryClient } from './QueryClient.js';
|
|
17
|
+
import { ClientError, ServerError, HttpURL, } from '@midnightntwrk/wallet-sdk-utilities/networking';
|
|
18
|
+
export const layer = (config) => Layer.effect(QueryClient, HttpURL.make(config.url).pipe(Effect.flatMap((url) => Effect.acquireRelease(Effect.sync(() => createClient({
|
|
19
|
+
url: url.toString(),
|
|
20
|
+
shouldRetry: (error, retries) => {
|
|
21
|
+
const statusCode = error.response?.status ?? 500;
|
|
22
|
+
return Promise.resolve(retries < 3 && statusCode >= 502 && statusCode <= 504);
|
|
23
|
+
},
|
|
24
|
+
})), (client) => Effect.sync(() => client.dispose()))), Effect.map((client) => new HttpQueryClientImpl(client))));
|
|
25
|
+
class HttpQueryClientImpl {
|
|
26
|
+
constructor(client) {
|
|
27
|
+
this.client = client;
|
|
28
|
+
}
|
|
29
|
+
client;
|
|
30
|
+
query(document, variables) {
|
|
31
|
+
return Effect.async((resume) => {
|
|
32
|
+
let result;
|
|
33
|
+
const dispose = this.client.subscribe({ query: print(document), variables: variables }, {
|
|
34
|
+
next: (data) => (result = data),
|
|
35
|
+
error: (error) => {
|
|
36
|
+
const statusCode = error.response?.status ?? 500;
|
|
37
|
+
const message = error.response?.statusText ?? 'An unknown error occurred';
|
|
38
|
+
resume(Effect.fail(statusCode >= 400 && statusCode < 500
|
|
39
|
+
? new ClientError({ message, cause: error })
|
|
40
|
+
: new ServerError({ message })));
|
|
41
|
+
},
|
|
42
|
+
complete: () => resume(result.errors
|
|
43
|
+
? Effect.fail(new ClientError({ message: result.errors[0].message, cause: result.errors }))
|
|
44
|
+
: Effect.succeed(result.data)),
|
|
45
|
+
});
|
|
46
|
+
// Ensure we dispose of the query if the running Fiber is terminated.
|
|
47
|
+
return Effect.sync(dispose);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Effect, Context } from 'effect';
|
|
2
|
+
import { type ClientError, type ServerError } from '@midnightntwrk/wallet-sdk-utilities/networking';
|
|
3
|
+
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
4
|
+
import { QueryClient } from './QueryClient.js';
|
|
5
|
+
/** Describes an invocable GraphQL query. */
|
|
6
|
+
export interface Query<R, V, F extends Query.QueryFn<R, V> = Query.QueryFn<R, V>> extends Effect.Effect<F> {
|
|
7
|
+
readonly tag: Context.Tag<Query<R, V>, F>;
|
|
8
|
+
readonly run: F;
|
|
9
|
+
}
|
|
10
|
+
export declare namespace Query {
|
|
11
|
+
/**
|
|
12
|
+
* A GraphQL query (that may be parameterized with variables), that returns a typed document.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* `Document` is a simple type alias for `TypedDocumentNode`.
|
|
16
|
+
* @typeParam R The type returned by the GraphQL query.
|
|
17
|
+
* @typeParam V A type that describes the variables present in the GraphQL query.
|
|
18
|
+
*/
|
|
19
|
+
type Document<R, V> = TypedDocumentNode<R, V>;
|
|
20
|
+
/** The variables of a {@link Document}. */
|
|
21
|
+
type Variables<T> = T extends Document<any, infer V> ? V : never;
|
|
22
|
+
/** The expected result of executing a {@link Document}. */
|
|
23
|
+
type Result<T> = T extends Document<infer R, any> ? R : never;
|
|
24
|
+
/**
|
|
25
|
+
* Describes a function that executes a GraphQL query over some given variables.
|
|
26
|
+
*
|
|
27
|
+
* @param variables The variables to be used in the GraphQL query.
|
|
28
|
+
* @returns An `Effect` that will yield the result of the GraphQL query.
|
|
29
|
+
*/
|
|
30
|
+
type QueryFn<R, V> = (variables: V) => Effect.Effect<R, ClientError | ServerError, QueryClient>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Constructs a new invocable GraphQL query.
|
|
34
|
+
*
|
|
35
|
+
* @param name The name of the tag to be associated with this query.
|
|
36
|
+
* @param document A parsed GraphQL query document that represents the query.
|
|
37
|
+
* @returns A {@link Query}.
|
|
38
|
+
*/
|
|
39
|
+
export declare const make: <Name extends string, T extends Query.Document<R, V>, R = Query.Result<T>, V = Query.Variables<T>>(name: Name, document: T) => Query<R, V>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// This file is part of MIDNIGHT-WALLET-SDK.
|
|
2
|
+
// Copyright (C) Midnight Foundation
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// You may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
import { Effect, Context, Effectable, Option, identity } from 'effect';
|
|
14
|
+
import { QueryClient } from './QueryClient.js';
|
|
15
|
+
/**
|
|
16
|
+
* Constructs a new invocable GraphQL query.
|
|
17
|
+
*
|
|
18
|
+
* @param name The name of the tag to be associated with this query.
|
|
19
|
+
* @param document A parsed GraphQL query document that represents the query.
|
|
20
|
+
* @returns A {@link Query}.
|
|
21
|
+
*/
|
|
22
|
+
export const make = (name, document) => new QueryImpl(`${name}Query`, document);
|
|
23
|
+
class QueryImpl extends Effectable.Class {
|
|
24
|
+
name;
|
|
25
|
+
document;
|
|
26
|
+
constructor(name, document) {
|
|
27
|
+
super();
|
|
28
|
+
this.document = document;
|
|
29
|
+
this.name = name;
|
|
30
|
+
this.tag = Context.GenericTag(name);
|
|
31
|
+
this.run = ((variables) => Effect.flatMap(this, (f) => f(variables)));
|
|
32
|
+
}
|
|
33
|
+
tag;
|
|
34
|
+
run;
|
|
35
|
+
commit() {
|
|
36
|
+
const self = this; // eslint-disable-line @typescript-eslint/no-this-alias
|
|
37
|
+
return Effect.gen(function* () {
|
|
38
|
+
return Option.match(yield* Effect.serviceOption(self.tag), {
|
|
39
|
+
onSome: identity,
|
|
40
|
+
onNone: () => self.defaultFn.bind(self),
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
defaultFn(variables) {
|
|
45
|
+
return QueryClient.pipe(Effect.flatMap((client) => client.query(this.document, variables)));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Effect, Context } from 'effect';
|
|
2
|
+
import { type ClientError, type ServerError } from '@midnightntwrk/wallet-sdk-utilities/networking';
|
|
3
|
+
import type { Query } from './Query.js';
|
|
4
|
+
declare const QueryClient_base: Context.TagClass<QueryClient, "@midnight-ntwrk/indexer-client#QueryClient", QueryClient.Service>;
|
|
5
|
+
export declare class QueryClient extends QueryClient_base {
|
|
6
|
+
}
|
|
7
|
+
export declare namespace QueryClient {
|
|
8
|
+
interface ServerConfig {
|
|
9
|
+
readonly url: URL | string;
|
|
10
|
+
}
|
|
11
|
+
interface Service {
|
|
12
|
+
query<R, V, T extends Query.Document<R, V> = Query.Document<R, V>>(document: T, variables: V): Effect.Effect<Query.Result<T>, ClientError | ServerError>;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// This file is part of MIDNIGHT-WALLET-SDK.
|
|
2
|
+
// Copyright (C) Midnight Foundation
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// You may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
import { Context } from 'effect';
|
|
14
|
+
export class QueryClient extends Context.Tag('@midnight-ntwrk/indexer-client#QueryClient')() {
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Query } from './Query.js';
|
|
2
|
+
import type { QueryClient } from './QueryClient.js';
|
|
3
|
+
/**
|
|
4
|
+
* Executes a {@link Query} as a `Promise` using the {@link HttpQueryClient}.
|
|
5
|
+
*
|
|
6
|
+
* @param query The query to execute.
|
|
7
|
+
* @param variables The variables to supply to the query.
|
|
8
|
+
* @param config The server configuration (URL) for the HTTP query client.
|
|
9
|
+
* @returns A `Promise` that resolves with the query result.
|
|
10
|
+
*/
|
|
11
|
+
export declare const runPromise: <R, V>(query: Query<R, V>, variables: V, config: QueryClient.ServerConfig) => Promise<R>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// This file is part of MIDNIGHT-WALLET-SDK.
|
|
2
|
+
// Copyright (C) Midnight Foundation
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// You may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
import { Effect } from 'effect';
|
|
14
|
+
import * as HttpQueryClient from './HttpQueryClient.js';
|
|
15
|
+
/**
|
|
16
|
+
* Executes a {@link Query} as a `Promise` using the {@link HttpQueryClient}.
|
|
17
|
+
*
|
|
18
|
+
* @param query The query to execute.
|
|
19
|
+
* @param variables The variables to supply to the query.
|
|
20
|
+
* @param config The server configuration (URL) for the HTTP query client.
|
|
21
|
+
* @returns A `Promise` that resolves with the query result.
|
|
22
|
+
*/
|
|
23
|
+
export const runPromise = (query, variables, config) => query.run(variables).pipe(Effect.provide(HttpQueryClient.layer(config)), Effect.scoped, Effect.runPromise);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Effect, Stream, Context } from 'effect';
|
|
2
|
+
import { type ClientError, type ServerError } from '@midnightntwrk/wallet-sdk-utilities/networking';
|
|
3
|
+
import { SubscriptionClient } from './SubscriptionClient.js';
|
|
4
|
+
import type { Query } from './Query.js';
|
|
5
|
+
/** Describes a subscription of elements from an invocable GraphQL query. */
|
|
6
|
+
export interface Subscription<R, V, F extends Subscription.SubscriptionFn<R, V> = Subscription.SubscriptionFn<R, V>> extends Effect.Effect<F> {
|
|
7
|
+
readonly tag: Context.Tag<Subscription<R, V>, F>;
|
|
8
|
+
readonly run: F;
|
|
9
|
+
/**
|
|
10
|
+
* Like {@link run}, but caps the in-flight item count by disposing the underlying GraphQL subscription when the
|
|
11
|
+
* consumer can't keep up, and re-opens it with `variables(cursor)` for the running monotonic cursor once the queue
|
|
12
|
+
* drains. Items are never dropped.
|
|
13
|
+
*/
|
|
14
|
+
readonly runWithBackpressure: (options: SubscriptionClient.BackpressureOptions<R, V>) => Stream.Stream<R, ClientError | ServerError, SubscriptionClient>;
|
|
15
|
+
}
|
|
16
|
+
export declare namespace Subscription {
|
|
17
|
+
/**
|
|
18
|
+
* Describes a function that streams a GraphQL subscription for some given variables.
|
|
19
|
+
*
|
|
20
|
+
* @param variables The variables to be used in the GraphQL query.
|
|
21
|
+
* @returns A `Stream` that will yield the elements of the GraphQL subscription.
|
|
22
|
+
*/
|
|
23
|
+
type SubscriptionFn<R, V> = (variables: V) => Stream.Stream<R, ClientError | ServerError, SubscriptionClient>;
|
|
24
|
+
}
|
|
25
|
+
export declare const make: <Name extends string, T extends Query.Document<R, V>, R = Query.Result<T>, V = Query.Variables<T>>(name: Name, document: T) => Subscription<R, V>;
|