@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.
Files changed (78) hide show
  1. package/README.md +86 -0
  2. package/dist/effect/Backpressure.d.ts +97 -0
  3. package/dist/effect/Backpressure.js +146 -0
  4. package/dist/effect/ConnectionHelper.d.ts +4 -0
  5. package/dist/effect/ConnectionHelper.js +41 -0
  6. package/dist/effect/HttpQueryClient.d.ts +4 -0
  7. package/dist/effect/HttpQueryClient.js +50 -0
  8. package/dist/effect/Query.d.ts +39 -0
  9. package/dist/effect/Query.js +47 -0
  10. package/dist/effect/QueryClient.d.ts +15 -0
  11. package/dist/effect/QueryClient.js +15 -0
  12. package/dist/effect/QueryRunner.d.ts +11 -0
  13. package/dist/effect/QueryRunner.js +23 -0
  14. package/dist/effect/Subscription.d.ts +25 -0
  15. package/dist/effect/Subscription.js +56 -0
  16. package/dist/effect/SubscriptionClient.d.ts +23 -0
  17. package/dist/effect/SubscriptionClient.js +15 -0
  18. package/dist/effect/WsSubscriptionClient.d.ts +4 -0
  19. package/dist/effect/WsSubscriptionClient.js +89 -0
  20. package/dist/effect/index.d.ts +8 -0
  21. package/dist/effect/index.js +20 -0
  22. package/dist/effect/test/backpressure.test.d.ts +1 -0
  23. package/dist/effect/test/backpressure.test.js +146 -0
  24. package/dist/effect/test/connectionHelper.test.d.ts +1 -0
  25. package/dist/effect/test/connectionHelper.test.js +34 -0
  26. package/dist/effect/test/httpQueryClient.spied.test.d.ts +1 -0
  27. package/dist/effect/test/httpQueryClient.spied.test.js +37 -0
  28. package/dist/effect/test/httpQueryClient.test.d.ts +1 -0
  29. package/dist/effect/test/httpQueryClient.test.js +32 -0
  30. package/dist/effect/test/queryRunner.spied.test.d.ts +1 -0
  31. package/dist/effect/test/queryRunner.spied.test.js +46 -0
  32. package/dist/effect/test/wsSubscriptionClient.spied.test.d.ts +1 -0
  33. package/dist/effect/test/wsSubscriptionClient.spied.test.js +37 -0
  34. package/dist/effect/test/wsSubscriptionClient.test.d.ts +1 -0
  35. package/dist/effect/test/wsSubscriptionClient.test.js +32 -0
  36. package/dist/graphql/generated/fragment-masking.d.ts +19 -0
  37. package/dist/graphql/generated/fragment-masking.js +16 -0
  38. package/dist/graphql/generated/gql.d.ts +61 -0
  39. package/dist/graphql/generated/gql.js +17 -0
  40. package/dist/graphql/generated/graphql.d.ts +1021 -0
  41. package/dist/graphql/generated/graphql.js +700 -0
  42. package/dist/graphql/generated/index.d.ts +2 -0
  43. package/dist/graphql/generated/index.js +2 -0
  44. package/dist/graphql/queries/BlockHash.d.ts +6 -0
  45. package/dist/graphql/queries/BlockHash.js +24 -0
  46. package/dist/graphql/queries/Connect.d.ts +6 -0
  47. package/dist/graphql/queries/Connect.js +19 -0
  48. package/dist/graphql/queries/Disconnect.d.ts +6 -0
  49. package/dist/graphql/queries/Disconnect.js +19 -0
  50. package/dist/graphql/queries/FetchTermsAndConditions.d.ts +6 -0
  51. package/dist/graphql/queries/FetchTermsAndConditions.js +26 -0
  52. package/dist/graphql/queries/TransactionHistoryDetail.d.ts +6 -0
  53. package/dist/graphql/queries/TransactionHistoryDetail.js +30 -0
  54. package/dist/graphql/queries/TransactionStatus.d.ts +6 -0
  55. package/dist/graphql/queries/TransactionStatus.js +34 -0
  56. package/dist/graphql/queries/index.d.ts +6 -0
  57. package/dist/graphql/queries/index.js +18 -0
  58. package/dist/graphql/queries/test/BlockHash.test.d.ts +1 -0
  59. package/dist/graphql/queries/test/BlockHash.test.js +84 -0
  60. package/dist/graphql/subscriptions/DustLedgerEvents.d.ts +6 -0
  61. package/dist/graphql/subscriptions/DustLedgerEvents.js +24 -0
  62. package/dist/graphql/subscriptions/ShieldedTransactions.d.ts +8 -0
  63. package/dist/graphql/subscriptions/ShieldedTransactions.js +54 -0
  64. package/dist/graphql/subscriptions/UnshieldedTransactions.d.ts +8 -0
  65. package/dist/graphql/subscriptions/UnshieldedTransactions.js +68 -0
  66. package/dist/graphql/subscriptions/ZswapEvents.d.ts +6 -0
  67. package/dist/graphql/subscriptions/ZswapEvents.js +24 -0
  68. package/dist/graphql/subscriptions/index.d.ts +4 -0
  69. package/dist/graphql/subscriptions/index.js +16 -0
  70. package/dist/graphql/subscriptions/test/ShieldedTransactions.test.d.ts +1 -0
  71. package/dist/graphql/subscriptions/test/ShieldedTransactions.test.js +55 -0
  72. package/dist/graphql/subscriptions/test/UnshieldedTransactions.test.d.ts +1 -0
  73. package/dist/graphql/subscriptions/test/UnshieldedTransactions.test.js +52 -0
  74. package/dist/graphql/subscriptions/test/ZswapEvents.test.d.ts +1 -0
  75. package/dist/graphql/subscriptions/test/ZswapEvents.test.js +50 -0
  76. package/dist/index.d.ts +3 -0
  77. package/dist/index.js +15 -0
  78. package/package.json +57 -0
@@ -0,0 +1,56 @@
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, Stream, Context, Effectable, Option, identity } from 'effect';
14
+ import { SubscriptionClient } from './SubscriptionClient.js';
15
+ export const make = (name, document) => new SubscriptionImpl(`${name}Subscription`, document);
16
+ class SubscriptionImpl extends Effectable.Class {
17
+ name;
18
+ document;
19
+ constructor(name, document) {
20
+ super();
21
+ this.document = document;
22
+ this.name = name;
23
+ this.tag = Context.GenericTag(name);
24
+ this.run = ((variables) => Stream.flatMap(this, (f) => f(variables)));
25
+ }
26
+ tag;
27
+ run;
28
+ commit() {
29
+ const self = this; // eslint-disable-line @typescript-eslint/no-this-alias
30
+ return Effect.gen(function* () {
31
+ return Option.match(yield* Effect.serviceOption(self.tag), {
32
+ onSome: identity,
33
+ onNone: () => self.defaultFn.bind(self),
34
+ });
35
+ });
36
+ }
37
+ runWithBackpressure(options) {
38
+ const self = this; // eslint-disable-line @typescript-eslint/no-this-alias
39
+ // Mirror the `run`/`commit` dispatch: if a caller has provided an override
40
+ // via `self.tag` (test mocks do this), fall back to it — those overrides
41
+ // can't speak backpressure, but tests don't need to. The override is
42
+ // invoked with the initial variables derived from the cursor, since the
43
+ // override doesn't know the cursor protocol. Without an override, take the
44
+ // bounded pause/resume path against the underlying client.
45
+ return Stream.unwrap(Effect.gen(function* () {
46
+ const tagged = yield* Effect.serviceOption(self.tag);
47
+ return Option.match(tagged, {
48
+ onSome: (fn) => fn(options.variables(options.from)),
49
+ onNone: () => SubscriptionClient.pipe(Stream.flatMap((client) => client.subscribeWithBackpressure(self.document, options))),
50
+ });
51
+ }));
52
+ }
53
+ defaultFn(variables) {
54
+ return SubscriptionClient.pipe(Stream.flatMap((client) => client.subscribe(this.document, variables)));
55
+ }
56
+ }
@@ -0,0 +1,23 @@
1
+ import { type Stream, Context } from 'effect';
2
+ import type { Query } from './Query.js';
3
+ import type { BackpressureOptions as BackpressureOptionsImpl } from './Backpressure.js';
4
+ import { type ClientError, type ServerError } from '@midnightntwrk/wallet-sdk-utilities/networking';
5
+ declare const SubscriptionClient_base: Context.TagClass<SubscriptionClient, "@midnight-ntwrk/indexer-client#SubscriptionClient", SubscriptionClient.Service>;
6
+ export declare class SubscriptionClient extends SubscriptionClient_base {
7
+ }
8
+ export declare namespace SubscriptionClient {
9
+ interface ServerConfig {
10
+ readonly url: URL | string;
11
+ readonly keepAlive?: number | undefined;
12
+ }
13
+ /**
14
+ * Options for {@link Service.subscribeWithBackpressure}. Canonical definition lives in
15
+ * {@link ./Backpressure.BackpressureOptions} — re-exported here for ergonomic access through the namespace.
16
+ */
17
+ type BackpressureOptions<R, V> = BackpressureOptionsImpl<R, V>;
18
+ interface Service {
19
+ subscribe<R, V, T extends Query.Document<R, V> = Query.Document<R, V>>(document: T, variables: V): Stream.Stream<Query.Result<T>, ClientError | ServerError>;
20
+ subscribeWithBackpressure<R, V, T extends Query.Document<R, V> = Query.Document<R, V>>(document: T, options: BackpressureOptions<Query.Result<T>, V>): Stream.Stream<Query.Result<T>, ClientError | ServerError>;
21
+ }
22
+ }
23
+ 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 SubscriptionClient extends Context.Tag('@midnight-ntwrk/indexer-client#SubscriptionClient')() {
15
+ }
@@ -0,0 +1,4 @@
1
+ import { Layer, type Scope } from 'effect';
2
+ import { SubscriptionClient } from './SubscriptionClient.js';
3
+ import { type InvalidProtocolSchemeError } from '@midnightntwrk/wallet-sdk-utilities/networking';
4
+ export declare const layer: (config: SubscriptionClient.ServerConfig) => Layer.Layer<SubscriptionClient, InvalidProtocolSchemeError, Scope.Scope>;
@@ -0,0 +1,89 @@
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, Stream, Layer, Array as Arr } from 'effect';
14
+ import { createClient } from 'graphql-ws';
15
+ import { print } from 'graphql';
16
+ import { SubscriptionClient } from './SubscriptionClient.js';
17
+ import { withBackpressure } from './Backpressure.js';
18
+ import { WsURL, ClientError, ServerError, } from '@midnightntwrk/wallet-sdk-utilities/networking';
19
+ // ============================================================================
20
+ // Error classification
21
+ // ============================================================================
22
+ // graphql-ws delivers a subscription sink error as one of: `Error`, `CloseEvent`,
23
+ // or `readonly GraphQLError[]` (per its Sink docs). Only the array form should
24
+ // surface as a ClientError; everything else is a transport/server failure.
25
+ const isGraphQLErrorArray = (err) => {
26
+ if (!Arr.isArray(err) || err.length === 0)
27
+ return false;
28
+ const first = err[0];
29
+ return typeof first === 'object' && first !== null && 'message' in first && typeof first.message === 'string';
30
+ };
31
+ const toClientOrServerError = (err) => isGraphQLErrorArray(err)
32
+ ? new ClientError({ message: err.map((e) => e.message).join('; '), cause: err })
33
+ : new ServerError({ message: String(err) });
34
+ // ============================================================================
35
+ // Layer / class
36
+ // ============================================================================
37
+ export const layer = (config) => Layer.scoped(SubscriptionClient, WsURL.make(config.url).pipe(Effect.flatMap((url) => Effect.acquireRelease(Effect.sync(() => createClient({ url: url.toString(), shouldRetry: () => false, keepAlive: config.keepAlive ?? 15_000 })), (client) => Effect.sync(() => client.dispose()))), Effect.map((client) => new WebSocketSubscriptionClientImpl(client))));
38
+ class WebSocketSubscriptionClientImpl {
39
+ constructor(client) {
40
+ this.client = client;
41
+ }
42
+ client;
43
+ subscribe(document, variables) {
44
+ // Stream.async forks a top-level fiber per emit.single (via
45
+ // Runtime.runPromiseExit), which leaks into Effect's Global.roots at the
46
+ // ~1k msg/sec rate the indexer pushes. Stream.asyncPush writes straight
47
+ // to an internal queue and tears it down with the surrounding scope.
48
+ return Stream.asyncPush((emit) => Effect.acquireRelease(Effect.sync(() => this.client.subscribe({ query: print(document), variables: variables }, {
49
+ next: (data) => {
50
+ if (data.errors) {
51
+ emit.fail(new ClientError({
52
+ message: data.errors.map((e) => e.message).join('; '),
53
+ cause: data.errors,
54
+ }));
55
+ }
56
+ else {
57
+ emit.single(data.data);
58
+ }
59
+ },
60
+ error: (err) => {
61
+ if (isGraphQLErrorArray(err)) {
62
+ emit.fail(new ClientError({ message: err.map((e) => e.message).join('; '), cause: err }));
63
+ }
64
+ else {
65
+ emit.fail(new ServerError({ message: String(err) }));
66
+ }
67
+ },
68
+ complete: () => {
69
+ emit.end();
70
+ },
71
+ })), (dispose) => Effect.sync(() => dispose())), { bufferSize: 'unbounded' });
72
+ }
73
+ subscribeWithBackpressure(document, options) {
74
+ const client = this.client;
75
+ // Adapt graphql-ws's Sink callbacks to the generic Source contract; the
76
+ // backpressure/lifecycle logic lives in `withBackpressure`.
77
+ const source = ({ variables, onItem, onError, onComplete }) => client.subscribe({ query: print(document), variables: variables }, {
78
+ next: (data) => {
79
+ if (data.errors)
80
+ onError(toClientOrServerError(data.errors));
81
+ else
82
+ onItem(data.data);
83
+ },
84
+ error: (err) => onError(toClientOrServerError(err)),
85
+ complete: onComplete,
86
+ });
87
+ return withBackpressure(source, options);
88
+ }
89
+ }
@@ -0,0 +1,8 @@
1
+ export * as Query from './Query.js';
2
+ export * as Subscription from './Subscription.js';
3
+ export * from './QueryClient.js';
4
+ export * as HttpQueryClient from './HttpQueryClient.js';
5
+ export * from './SubscriptionClient.js';
6
+ export * as WsSubscriptionClient from './WsSubscriptionClient.js';
7
+ export * as ConnectionHelper from './ConnectionHelper.js';
8
+ export * as QueryRunner from './QueryRunner.js';
@@ -0,0 +1,20 @@
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
+ export * as Query from './Query.js';
14
+ export * as Subscription from './Subscription.js';
15
+ export * from './QueryClient.js';
16
+ export * as HttpQueryClient from './HttpQueryClient.js';
17
+ export * from './SubscriptionClient.js';
18
+ export * as WsSubscriptionClient from './WsSubscriptionClient.js';
19
+ export * as ConnectionHelper from './ConnectionHelper.js';
20
+ export * as QueryRunner from './QueryRunner.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -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
+ import { describe, it, expect } from 'vitest';
14
+ import { initialBPState, decideItem, decideTerminate, decideComplete, decideResume, } from '../Backpressure.js';
15
+ const state = (overrides = {}) => ({
16
+ ...initialBPState(0n),
17
+ ...overrides,
18
+ });
19
+ describe('Backpressure state machine', () => {
20
+ describe('decideItem', () => {
21
+ it('emits and advances watermark when key is strictly greater than lastKey', () => {
22
+ const [decision, next] = decideItem(state({ lastKey: 5n }), 6n, 0, 1000);
23
+ expect(decision).toEqual({ emit: true, pause: false });
24
+ expect(next.lastKey).toBe(6n);
25
+ expect(next.inFlight).toBe(1);
26
+ });
27
+ it('drops the item at `from` on the initial session (exclusive watermark)', () => {
28
+ // The watermark seeds from `from`, so the item whose key equals `from` is treated as
29
+ // already-seen. A caller that wants the item at its logical cursor emitted seeds `from` one
30
+ // below it (e.g. a wallet passes `appliedIndex - 1` to still receive the boundary event).
31
+ const before = initialBPState(5n);
32
+ const [decision, next] = decideItem(before, 5n, 0, 1000);
33
+ expect(decision.emit).toBe(false);
34
+ expect(next).toBe(before);
35
+ });
36
+ it('drops a re-delivered boundary equal to lastKey (inclusive-cursor dedup)', () => {
37
+ // An inclusive resume cursor re-delivers the last-emitted key; the exclusive watermark drops
38
+ // it so it is not emitted twice.
39
+ const before = state({ lastKey: 5n, inFlight: 0 });
40
+ const [decision, next] = decideItem(before, 5n, 0, 1000);
41
+ expect(decision.emit).toBe(false);
42
+ expect(next).toBe(before);
43
+ });
44
+ it('drops items below lastKey', () => {
45
+ const before = state({ lastKey: 5n });
46
+ const [decision, next] = decideItem(before, 3n, 0, 1000);
47
+ expect(decision.emit).toBe(false);
48
+ expect(next).toBe(before);
49
+ });
50
+ it('drops items from a stale generation', () => {
51
+ const before = state({ generation: 2 });
52
+ const [decision, next] = decideItem(before, 1n, 1, 1000);
53
+ expect(decision.emit).toBe(false);
54
+ expect(next).toBe(before);
55
+ });
56
+ it('drops items when terminal', () => {
57
+ const before = state({ terminal: true });
58
+ const [decision, next] = decideItem(before, 1n, 0, 1000);
59
+ expect(decision.emit).toBe(false);
60
+ expect(next).toBe(before);
61
+ });
62
+ it('sets pause when inFlight reaches bufferSize', () => {
63
+ const [decision, next] = decideItem(state({ lastKey: 5n, inFlight: 2 }), 6n, 0, 3);
64
+ expect(decision).toEqual({ emit: true, pause: true });
65
+ expect(next.paused).toBe(true);
66
+ expect(next.inFlight).toBe(3);
67
+ });
68
+ it('does not re-pause when already paused', () => {
69
+ const [decision, next] = decideItem(state({ lastKey: 5n, inFlight: 3, paused: true }), 6n, 0, 3);
70
+ expect(decision).toEqual({ emit: true, pause: false });
71
+ expect(next.paused).toBe(true);
72
+ });
73
+ });
74
+ describe('decideTerminate', () => {
75
+ it('surfaces a terminal event and marks state terminal', () => {
76
+ const [shouldSurface, next] = decideTerminate(state(), 0);
77
+ expect(shouldSurface).toBe(true);
78
+ expect(next.terminal).toBe(true);
79
+ });
80
+ it('swallows terminal events from a stale generation', () => {
81
+ const before = state({ generation: 2 });
82
+ const [shouldSurface, next] = decideTerminate(before, 1);
83
+ expect(shouldSurface).toBe(false);
84
+ expect(next).toBe(before);
85
+ });
86
+ it('is idempotent — already-terminal state returns false', () => {
87
+ const before = state({ terminal: true });
88
+ const [shouldSurface, next] = decideTerminate(before, 0);
89
+ expect(shouldSurface).toBe(false);
90
+ expect(next).toBe(before);
91
+ });
92
+ });
93
+ describe('decideComplete', () => {
94
+ it('signals end-of-stream when not paused, stale, or terminal', () => {
95
+ const [shouldEnd, next] = decideComplete(state(), 0);
96
+ expect(shouldEnd).toBe(true);
97
+ expect(next.terminal).toBe(true);
98
+ });
99
+ it('treats paused completion as dispose-induced (no end)', () => {
100
+ // This is the regression case: without the paused check, a dispose-on-pause
101
+ // would deliver a `complete:` from the old session and terminate the
102
+ // consumer-visible stream mid-resume.
103
+ const before = state({ paused: true });
104
+ const [shouldEnd, next] = decideComplete(before, 0);
105
+ expect(shouldEnd).toBe(false);
106
+ expect(next).toBe(before);
107
+ });
108
+ it('swallows completions from a stale generation', () => {
109
+ const before = state({ generation: 2 });
110
+ const [shouldEnd, next] = decideComplete(before, 1);
111
+ expect(shouldEnd).toBe(false);
112
+ expect(next).toBe(before);
113
+ });
114
+ });
115
+ describe('decideResume', () => {
116
+ it('resumes and bumps generation when paused and drained to threshold', () => {
117
+ const [shouldResume, next] = decideResume(state({ paused: true, inFlight: 3, generation: 4 }), 2);
118
+ expect(shouldResume).toBe(true);
119
+ expect(next).toMatchObject({ paused: false, inFlight: 2, generation: 5 });
120
+ });
121
+ it('decrements inFlight even when not resuming (drain accounting)', () => {
122
+ const [shouldResume, next] = decideResume(state({ paused: false, inFlight: 7 }), 2);
123
+ expect(shouldResume).toBe(false);
124
+ expect(next.inFlight).toBe(6);
125
+ });
126
+ it('does not resume when above threshold', () => {
127
+ const [shouldResume, next] = decideResume(state({ paused: true, inFlight: 5 }), 2);
128
+ expect(shouldResume).toBe(false);
129
+ expect(next).toMatchObject({ paused: true, inFlight: 4 });
130
+ });
131
+ it('does not resume when terminal', () => {
132
+ const before = state({ paused: true, inFlight: 3, terminal: true });
133
+ const [shouldResume, next] = decideResume(before, 2);
134
+ expect(shouldResume).toBe(false);
135
+ expect(next).toBe(before);
136
+ });
137
+ it('clamps inFlight at zero (defensive against drain underflow)', () => {
138
+ const [, next] = decideResume(state({ inFlight: 0 }), 2);
139
+ expect(next.inFlight).toBe(0);
140
+ });
141
+ it('does not resume when not paused, even if drained', () => {
142
+ const [shouldResume] = decideResume(state({ paused: false, inFlight: 0 }), 2);
143
+ expect(shouldResume).toBe(false);
144
+ });
145
+ });
146
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
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 { Either } from 'effect';
14
+ import { describe, expect, it } from 'vitest';
15
+ import { createWebSocketUrl, deriveWebSocketUrl } from '../ConnectionHelper.js';
16
+ const right = (e) => (Either.isRight(e) ? e.right : undefined);
17
+ describe('deriveWebSocketUrl', () => {
18
+ it('flips http→ws and https→wss', () => {
19
+ expect(right(deriveWebSocketUrl('http://example.com/api'))).toBe('ws://example.com/api/ws');
20
+ expect(right(deriveWebSocketUrl('https://example.com/api'))).toBe('wss://example.com/api/ws');
21
+ });
22
+ it('appends /ws regardless of whether the input path ends with a slash', () => {
23
+ expect(right(deriveWebSocketUrl('http://example.com/api'))).toBe('ws://example.com/api/ws');
24
+ expect(right(deriveWebSocketUrl('http://example.com/api/'))).toBe('ws://example.com/api/ws');
25
+ });
26
+ });
27
+ describe('createWebSocketUrl', () => {
28
+ it('returns the provided wsUrl as-is, ignoring httpUrl', () => {
29
+ expect(right(createWebSocketUrl('http://example.com/api', 'ws://override.example/custom'))).toBe('ws://override.example/custom');
30
+ });
31
+ it('falls back to deriveWebSocketUrl when wsUrl is undefined', () => {
32
+ expect(right(createWebSocketUrl('https://example.com/api'))).toBe('wss://example.com/api/ws');
33
+ });
34
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
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 { describe, expect, it, vi } from 'vitest';
15
+ import * as QueryClient from '../QueryClient.js';
16
+ vi.mock('graphql-http', () => ({
17
+ createClient: vi.fn(),
18
+ }));
19
+ describe('HttpQueryClient', () => {
20
+ describe('layer', () => {
21
+ it('disposes of underlying scoped client', async () => {
22
+ const graphqlHTTP = await import('graphql-http');
23
+ const SpiedHttpQueryClient = await import('../HttpQueryClient.js');
24
+ const dispose = vi.fn();
25
+ const spiedCreateClient = vi
26
+ .spyOn(graphqlHTTP, 'createClient')
27
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any
28
+ .mockImplementationOnce(() => ({ dispose }));
29
+ await Effect.gen(function* () {
30
+ const client = yield* QueryClient.QueryClient;
31
+ expect(client).toBeDefined();
32
+ }).pipe(Effect.provide(SpiedHttpQueryClient.layer({ url: 'http://localhost.com' })), Effect.scoped, Effect.runPromise);
33
+ expect(spiedCreateClient).toHaveBeenCalled();
34
+ expect(dispose).toHaveBeenCalled();
35
+ });
36
+ });
37
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
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 { describe, it } from 'vitest';
15
+ import * as QueryClient from '../QueryClient.js';
16
+ import * as HttpQueryClient from '../HttpQueryClient.js';
17
+ describe('HttpQueryClient', () => {
18
+ describe('layer', () => {
19
+ // Ensures that we cannot construct a layer for HttpQueryClient when we use common incorrect URI schemes.
20
+ it.each(['ftp:', 'mailto:', 'ws:', 'wss:', 'file:'])('should fail when constructed with %s as the URI scheme', async (scheme) => {
21
+ await Effect.gen(function* () {
22
+ // We should never be able to resolve a QueryClient since the configuration used to create the
23
+ // associated HttpQueryClient layer is invalid with the protocol schemes being used.
24
+ return yield* QueryClient.QueryClient;
25
+ }).pipe(Effect.flatMap((_) => Effect.fail('Unexpectedly resolved a QueryClient')), Effect.provide(HttpQueryClient.layer({ url: `${scheme}//localhost.com` })),
26
+ // Ensure the reported invalid protocol scheme is the one used.
27
+ Effect.catchTag('InvalidProtocolSchemeError', (err) => err.invalidScheme !== scheme
28
+ ? Effect.fail(`Expected '${scheme}' but received '${err.invalidScheme}'`)
29
+ : Effect.succeed(void 0)), Effect.scoped, Effect.runPromise);
30
+ });
31
+ });
32
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
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 { describe, expect, it, vi } from 'vitest';
14
+ import { BlockHash } from '../../graphql/queries/BlockHash.js';
15
+ vi.mock('graphql-http', () => ({
16
+ createClient: vi.fn(),
17
+ }));
18
+ describe('QueryRunner', () => {
19
+ describe('runPromise', () => {
20
+ it('should resolve with query result', async () => {
21
+ const graphqlHTTP = await import('graphql-http');
22
+ const SpiedQueryRunner = await import('../QueryRunner.js');
23
+ const block = {
24
+ block: { height: 1_000, hash: 'SOME_HASH', ledgerParameters: '0x0', timestamp: 1 },
25
+ };
26
+ const dispose = vi.fn();
27
+ const spiedCreateClient = vi
28
+ .spyOn(graphqlHTTP, 'createClient')
29
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any
30
+ .mockImplementationOnce(() => ({ subscribe, dispose }));
31
+ function subscribe(_params, sink) {
32
+ sink.next({ data: block });
33
+ sink.complete();
34
+ return vi.fn();
35
+ }
36
+ const result = await SpiedQueryRunner.runPromise(BlockHash, { offset: null }, { url: 'http://localhost:8088/api/v4/graphql' });
37
+ expect(result).toEqual(block);
38
+ expect(spiedCreateClient).toHaveBeenCalled();
39
+ expect(dispose).toHaveBeenCalled();
40
+ });
41
+ it('should reject for invalid URL scheme', async () => {
42
+ const SpiedQueryRunner = await import('../QueryRunner.js');
43
+ await expect(SpiedQueryRunner.runPromise(BlockHash, { offset: null }, { url: 'ftp://localhost:8088/api/v4/graphql' })).rejects.toThrow();
44
+ });
45
+ });
46
+ });
@@ -0,0 +1,37 @@
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 { describe, expect, it, vi } from 'vitest';
15
+ import * as SubscriptionClient from '../SubscriptionClient.js';
16
+ vi.mock('graphql-ws', () => ({
17
+ createClient: vi.fn(),
18
+ }));
19
+ describe('WsSubscriptionClient', () => {
20
+ describe('layer', () => {
21
+ it('disposes of underlying scoped client', async () => {
22
+ const graphqlWS = await import('graphql-ws');
23
+ const SpiedWsSubscriptionClient = await import('../WsSubscriptionClient.js');
24
+ const dispose = vi.fn();
25
+ const spiedCreateClient = vi
26
+ .spyOn(graphqlWS, 'createClient')
27
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any
28
+ .mockImplementationOnce(() => ({ dispose }));
29
+ await Effect.gen(function* () {
30
+ const client = yield* SubscriptionClient.SubscriptionClient;
31
+ expect(client).toBeDefined();
32
+ }).pipe(Effect.provide(SpiedWsSubscriptionClient.layer({ url: 'ws://localhost.com' })), Effect.scoped, Effect.runPromise);
33
+ expect(spiedCreateClient).toHaveBeenCalled();
34
+ expect(dispose).toHaveBeenCalled();
35
+ });
36
+ });
37
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
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 { describe, it } from 'vitest';
15
+ import * as SubscriptionClient from '../SubscriptionClient.js';
16
+ import * as WsSubscriptionClient from '../WsSubscriptionClient.js';
17
+ describe('WsSubscriptionClient', () => {
18
+ describe('layer', () => {
19
+ // Ensures that we cannot construct a layer for WsSubscriptionClient when we use common incorrect URI schemes.
20
+ it.each(['ftp:', 'mailto:', 'http:', 'https:', 'file:'])('should fail when constructed with %s as the URI scheme', async (scheme) => {
21
+ await Effect.gen(function* () {
22
+ // We should never be able to resolve a SubscriptionClient since the configuration used to create the
23
+ // associated WsSubscriptionClient layer is invalid with the protocol schemes being used.
24
+ return yield* SubscriptionClient.SubscriptionClient;
25
+ }).pipe(Effect.flatMap((_) => Effect.fail('Unexpectedly resolved a SubscriptionClient')), Effect.provide(WsSubscriptionClient.layer({ url: `${scheme}//localhost.com` })),
26
+ // Ensure the reported invalid protocol scheme is the one used.
27
+ Effect.catchTag('InvalidProtocolSchemeError', (err) => err.invalidScheme !== scheme
28
+ ? Effect.fail(`Expected '${scheme}' but received '${err.invalidScheme}'`)
29
+ : Effect.succeed(void 0)), Effect.scoped, Effect.runPromise);
30
+ });
31
+ });
32
+ });
@@ -0,0 +1,19 @@
1
+ import type { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
2
+ import type { Incremental } from './graphql.js';
3
+ export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> = TDocumentType extends DocumentTypeDecoration<infer TType, any> ? [TType] extends [{
4
+ ' $fragmentName'?: infer TKey;
5
+ }] ? TKey extends string ? {
6
+ ' $fragmentRefs'?: {
7
+ [key in TKey]: TType;
8
+ };
9
+ } : never : never : never;
10
+ export declare function useFragment<TType>(_documentNode: DocumentTypeDecoration<TType, any>, fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>): TType;
11
+ export declare function useFragment<TType>(_documentNode: DocumentTypeDecoration<TType, any>, fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | undefined): TType | undefined;
12
+ export declare function useFragment<TType>(_documentNode: DocumentTypeDecoration<TType, any>, fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null): TType | null;
13
+ export declare function useFragment<TType>(_documentNode: DocumentTypeDecoration<TType, any>, fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined): TType | null | undefined;
14
+ export declare function useFragment<TType>(_documentNode: DocumentTypeDecoration<TType, any>, fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>>): Array<TType>;
15
+ export declare function useFragment<TType>(_documentNode: DocumentTypeDecoration<TType, any>, fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined): Array<TType> | null | undefined;
16
+ export declare function useFragment<TType>(_documentNode: DocumentTypeDecoration<TType, any>, fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>): ReadonlyArray<TType>;
17
+ export declare function useFragment<TType>(_documentNode: DocumentTypeDecoration<TType, any>, fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined): ReadonlyArray<TType> | null | undefined;
18
+ export declare function makeFragmentData<F extends DocumentTypeDecoration<any, any>, FT extends ResultOf<F>>(data: FT, _fragment: F): FragmentType<F>;
19
+ export declare function isFragmentReady<TQuery, TFrag>(queryNode: DocumentTypeDecoration<TQuery, any>, fragmentNode: TypedDocumentNode<TFrag>, data: FragmentType<TypedDocumentNode<Incremental<TFrag>, any>> | null | undefined): data is FragmentType<typeof fragmentNode>;