@fedimint/core-web 0.0.0-20250617190659
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/LICENSE +21 -0
- package/README.md +16 -0
- package/dist/dts/FedimintWallet.d.ts +114 -0
- package/dist/dts/FedimintWallet.d.ts.map +1 -0
- package/dist/dts/index.d.ts +3 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/dts/services/BalanceService.d.ts +15 -0
- package/dist/dts/services/BalanceService.d.ts.map +1 -0
- package/dist/dts/services/FederationService.d.ts +11 -0
- package/dist/dts/services/FederationService.d.ts.map +1 -0
- package/dist/dts/services/LightningService.d.ts +47 -0
- package/dist/dts/services/LightningService.d.ts.map +1 -0
- package/dist/dts/services/MintService.d.ts +23 -0
- package/dist/dts/services/MintService.d.ts.map +1 -0
- package/dist/dts/services/RecoveryService.d.ts +13 -0
- package/dist/dts/services/RecoveryService.d.ts.map +1 -0
- package/dist/dts/services/WalletService.d.ts +8 -0
- package/dist/dts/services/WalletService.d.ts.map +1 -0
- package/dist/dts/services/index.d.ts +7 -0
- package/dist/dts/services/index.d.ts.map +1 -0
- package/dist/dts/types/index.d.ts +4 -0
- package/dist/dts/types/index.d.ts.map +1 -0
- package/dist/dts/types/utils.d.ts +23 -0
- package/dist/dts/types/utils.d.ts.map +1 -0
- package/dist/dts/types/wallet.d.ts +102 -0
- package/dist/dts/types/wallet.d.ts.map +1 -0
- package/dist/dts/types/worker.d.ts +4 -0
- package/dist/dts/types/worker.d.ts.map +1 -0
- package/dist/dts/utils/logger.d.ts +17 -0
- package/dist/dts/utils/logger.d.ts.map +1 -0
- package/dist/dts/worker/WorkerClient.d.ts +44 -0
- package/dist/dts/worker/WorkerClient.d.ts.map +1 -0
- package/dist/dts/worker/index.d.ts +2 -0
- package/dist/dts/worker/index.d.ts.map +1 -0
- package/dist/index.d.ts +392 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/worker.js +2 -0
- package/dist/worker.js.map +1 -0
- package/package.json +43 -0
- package/src/FedimintWallet.test.ts +73 -0
- package/src/FedimintWallet.ts +215 -0
- package/src/index.ts +2 -0
- package/src/services/BalanceService.test.ts +50 -0
- package/src/services/BalanceService.ts +29 -0
- package/src/services/FederationService.test.ts +58 -0
- package/src/services/FederationService.ts +24 -0
- package/src/services/LightningService.test.ts +208 -0
- package/src/services/LightningService.ts +274 -0
- package/src/services/MintService.test.ts +74 -0
- package/src/services/MintService.ts +129 -0
- package/src/services/RecoveryService.ts +28 -0
- package/src/services/WalletService.test.ts +24 -0
- package/src/services/WalletService.ts +10 -0
- package/src/services/index.ts +6 -0
- package/src/test/TestFedimintWallet.ts +26 -0
- package/src/test/TestingService.ts +79 -0
- package/src/test/crypto.ts +44 -0
- package/src/test/fixtures.test.ts +18 -0
- package/src/test/fixtures.ts +70 -0
- package/src/types/index.ts +3 -0
- package/src/types/utils.ts +29 -0
- package/src/types/wallet.ts +141 -0
- package/src/types/worker.ts +16 -0
- package/src/utils/logger.ts +61 -0
- package/src/worker/WorkerClient.test.ts +6 -0
- package/src/worker/WorkerClient.ts +236 -0
- package/src/worker/index.ts +1 -0
- package/src/worker/worker.js +167 -0
- package/src/worker/worker.test.ts +90 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { expect } from 'vitest'
|
|
2
|
+
import { walletTest } from './fixtures'
|
|
3
|
+
|
|
4
|
+
walletTest('Fund wallet 1', async ({ fundedWallet }) => {
|
|
5
|
+
expect(fundedWallet).toBeDefined()
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
walletTest('Fund wallet 2', async ({ fundedWallet }) => {
|
|
9
|
+
expect(fundedWallet).toBeDefined()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
walletTest('Fund wallet 3', async ({ fundedWallet }) => {
|
|
13
|
+
expect(fundedWallet).toBeDefined()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
walletTest('Fund wallet 4', async ({ fundedWallet }) => {
|
|
17
|
+
expect(fundedWallet).toBeDefined()
|
|
18
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { expect, test } from 'vitest'
|
|
2
|
+
import { TestFedimintWallet } from './TestFedimintWallet'
|
|
3
|
+
import { WorkerClient } from '../worker/WorkerClient'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Adds Fixtures for setting up and tearing down a test FedimintWallet instance
|
|
7
|
+
*/
|
|
8
|
+
export const walletTest = test.extend<{
|
|
9
|
+
wallet: TestFedimintWallet
|
|
10
|
+
fundedWallet: TestFedimintWallet
|
|
11
|
+
unopenedWallet: TestFedimintWallet
|
|
12
|
+
}>({
|
|
13
|
+
wallet: async ({}, use) => {
|
|
14
|
+
const randomTestingId = Math.random().toString(36).substring(2, 15)
|
|
15
|
+
const wallet = new TestFedimintWallet()
|
|
16
|
+
expect(wallet).toBeDefined()
|
|
17
|
+
const inviteCode = await wallet.testing.getInviteCode()
|
|
18
|
+
await expect(
|
|
19
|
+
wallet.joinFederation(inviteCode, randomTestingId),
|
|
20
|
+
).resolves.toBe(true)
|
|
21
|
+
|
|
22
|
+
await use(wallet)
|
|
23
|
+
|
|
24
|
+
// clear up browser resources
|
|
25
|
+
await wallet.cleanup()
|
|
26
|
+
|
|
27
|
+
// remove the wallet db
|
|
28
|
+
await new Promise((resolve) => {
|
|
29
|
+
const request = indexedDB.deleteDatabase(randomTestingId)
|
|
30
|
+
request.onsuccess = resolve
|
|
31
|
+
request.onerror = resolve
|
|
32
|
+
request.onblocked = resolve
|
|
33
|
+
})
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
fundedWallet: async ({ wallet }, use) => {
|
|
37
|
+
await wallet.fundWallet(10_000)
|
|
38
|
+
await use(wallet)
|
|
39
|
+
},
|
|
40
|
+
unopenedWallet: async ({}, use) => {
|
|
41
|
+
const wallet = new TestFedimintWallet()
|
|
42
|
+
await wallet.initialize()
|
|
43
|
+
await use(wallet)
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Adds Fixtures for setting up and tearing down a test Worker instance
|
|
49
|
+
*/
|
|
50
|
+
export const workerTest = test.extend<{
|
|
51
|
+
worker: Worker
|
|
52
|
+
clientName: string
|
|
53
|
+
workerClient: WorkerClient
|
|
54
|
+
}>({
|
|
55
|
+
worker: async ({}, use) => {
|
|
56
|
+
const worker = new Worker(new URL('../worker/worker.js', import.meta.url), {
|
|
57
|
+
type: 'module',
|
|
58
|
+
})
|
|
59
|
+
await use(worker)
|
|
60
|
+
worker.terminate()
|
|
61
|
+
},
|
|
62
|
+
clientName: async ({}, use) => {
|
|
63
|
+
const randomTestingId = Math.random().toString(36).substring(2, 15)
|
|
64
|
+
await use(randomTestingId)
|
|
65
|
+
},
|
|
66
|
+
workerClient: async ({}, use) => {
|
|
67
|
+
const workerClient = new WorkerClient()
|
|
68
|
+
await use(workerClient)
|
|
69
|
+
},
|
|
70
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
type Alias<T> = T & {}
|
|
2
|
+
type Resolve<T> = T & unknown
|
|
3
|
+
|
|
4
|
+
type Seconds = Alias<number>
|
|
5
|
+
type Nanos = Alias<number>
|
|
6
|
+
|
|
7
|
+
type Duration = {
|
|
8
|
+
nanos: Nanos
|
|
9
|
+
secs: Seconds
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type MSats = Alias<number>
|
|
13
|
+
type Sats = Alias<number>
|
|
14
|
+
|
|
15
|
+
type JSONValue =
|
|
16
|
+
| string
|
|
17
|
+
| number
|
|
18
|
+
| boolean
|
|
19
|
+
| null
|
|
20
|
+
| { [key: string]: JSONValue }
|
|
21
|
+
| JSONValue[]
|
|
22
|
+
|
|
23
|
+
type JSONObject = Record<string, JSONValue>
|
|
24
|
+
|
|
25
|
+
type Result<T, U = string> =
|
|
26
|
+
| { success: true; data?: T }
|
|
27
|
+
| { success: false; error: U }
|
|
28
|
+
|
|
29
|
+
export { Alias, Resolve, Duration, MSats, Sats, JSONValue, JSONObject, Result }
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { MSats, Duration, JSONValue, JSONObject } from './utils'
|
|
2
|
+
|
|
3
|
+
const MODULE_KINDS = ['', 'ln', 'mint', 'wallet'] as const
|
|
4
|
+
type ModuleKind = (typeof MODULE_KINDS)[number]
|
|
5
|
+
|
|
6
|
+
// TODO: Define the structure of FederationConfig
|
|
7
|
+
type FederationConfig = JSONObject
|
|
8
|
+
|
|
9
|
+
type GatewayInfo = {
|
|
10
|
+
gateway_id: string
|
|
11
|
+
api: string
|
|
12
|
+
node_pub_key: string
|
|
13
|
+
federation_index: number
|
|
14
|
+
route_hints: RouteHint[]
|
|
15
|
+
fees: FeeToAmount
|
|
16
|
+
}
|
|
17
|
+
type LightningGateway = {
|
|
18
|
+
info: GatewayInfo
|
|
19
|
+
vetted: boolean
|
|
20
|
+
ttl: Duration
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type RouteHint = {
|
|
24
|
+
// TODO: Define the structure of RouteHint
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type FeeToAmount = {
|
|
28
|
+
// TODO: Define the structure of FeeToAmount
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type OutgoingLightningPayment = {
|
|
32
|
+
payment_type: PayType
|
|
33
|
+
contract_id: string
|
|
34
|
+
fee: MSats
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type PayType = { lightning: string } | { internal: string }
|
|
38
|
+
|
|
39
|
+
type LnPayState =
|
|
40
|
+
| 'created'
|
|
41
|
+
| 'canceled'
|
|
42
|
+
| { funded: { block_height: number } }
|
|
43
|
+
| { waiting_for_refund: { error_reason: string } }
|
|
44
|
+
| 'awaiting_change'
|
|
45
|
+
| { success: { preimage: string } }
|
|
46
|
+
| { refunded: { gateway_error: string } }
|
|
47
|
+
| { unexpected_error: { error_message: string } }
|
|
48
|
+
|
|
49
|
+
type LnReceiveState =
|
|
50
|
+
| 'created'
|
|
51
|
+
| { waiting_for_payment: { invoice: string; timeout: number } }
|
|
52
|
+
| { canceled: { reason: string } }
|
|
53
|
+
| 'funded'
|
|
54
|
+
| 'awaiting_funds'
|
|
55
|
+
| 'claimed'
|
|
56
|
+
|
|
57
|
+
type CreateBolt11Response = {
|
|
58
|
+
operation_id: string
|
|
59
|
+
invoice: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type StreamError = {
|
|
63
|
+
error: string
|
|
64
|
+
data: never
|
|
65
|
+
end: never
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type StreamSuccess<T extends JSONValue> = {
|
|
69
|
+
data: T
|
|
70
|
+
error: never
|
|
71
|
+
end: never
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type StreamEnd = {
|
|
75
|
+
end: string
|
|
76
|
+
data: never
|
|
77
|
+
error: never
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type StreamResult<T extends JSONValue> =
|
|
81
|
+
| StreamSuccess<T>
|
|
82
|
+
| StreamError
|
|
83
|
+
| StreamEnd
|
|
84
|
+
|
|
85
|
+
type CancelFunction = () => void
|
|
86
|
+
|
|
87
|
+
type ReissueExternalNotesState = 'Created' | 'Issuing' | 'Done'
|
|
88
|
+
// | { Failed: { error: string } }
|
|
89
|
+
|
|
90
|
+
type MintSpendNotesResponse = Array<string>
|
|
91
|
+
|
|
92
|
+
type SpendNotesState =
|
|
93
|
+
| 'Created'
|
|
94
|
+
| 'UserCanceledProcessing'
|
|
95
|
+
| 'UserCanceledSuccess'
|
|
96
|
+
| 'UserCanceledFailure'
|
|
97
|
+
| 'Success'
|
|
98
|
+
| 'Refunded'
|
|
99
|
+
|
|
100
|
+
type TxOutputSummary = {
|
|
101
|
+
outpoint: {
|
|
102
|
+
txid: string
|
|
103
|
+
vout: number
|
|
104
|
+
}
|
|
105
|
+
amount: number
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
type WalletSummary = {
|
|
109
|
+
spendable_utxos: TxOutputSummary[]
|
|
110
|
+
unsigned_peg_out_txos: TxOutputSummary[]
|
|
111
|
+
unsigned_change_utxos: TxOutputSummary[]
|
|
112
|
+
unconfirmed_peg_out_txos: TxOutputSummary[]
|
|
113
|
+
unconfirmed_change_utxos: TxOutputSummary[]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Keys are powers of 2 */
|
|
117
|
+
type NoteCountByDenomination = Record<number, number>
|
|
118
|
+
|
|
119
|
+
export {
|
|
120
|
+
LightningGateway,
|
|
121
|
+
FederationConfig,
|
|
122
|
+
RouteHint,
|
|
123
|
+
FeeToAmount,
|
|
124
|
+
OutgoingLightningPayment,
|
|
125
|
+
PayType,
|
|
126
|
+
LnPayState,
|
|
127
|
+
LnReceiveState,
|
|
128
|
+
CreateBolt11Response,
|
|
129
|
+
GatewayInfo,
|
|
130
|
+
StreamError,
|
|
131
|
+
StreamSuccess,
|
|
132
|
+
StreamResult,
|
|
133
|
+
ModuleKind,
|
|
134
|
+
CancelFunction,
|
|
135
|
+
ReissueExternalNotesState,
|
|
136
|
+
MintSpendNotesResponse,
|
|
137
|
+
SpendNotesState,
|
|
138
|
+
WalletSummary,
|
|
139
|
+
TxOutputSummary,
|
|
140
|
+
NoteCountByDenomination,
|
|
141
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const WorkerMessageTypes = [
|
|
2
|
+
'init',
|
|
3
|
+
'initialized',
|
|
4
|
+
'rpc',
|
|
5
|
+
'log',
|
|
6
|
+
'open',
|
|
7
|
+
'join',
|
|
8
|
+
'error',
|
|
9
|
+
'unsubscribe',
|
|
10
|
+
'cleanup',
|
|
11
|
+
'parseInviteCode',
|
|
12
|
+
'parseBolt11Invoice',
|
|
13
|
+
'previewFederation',
|
|
14
|
+
] as const
|
|
15
|
+
|
|
16
|
+
export type WorkerMessageType = (typeof WorkerMessageTypes)[number]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const logLevels = ['debug', 'info', 'warn', 'error', 'none'] as const
|
|
2
|
+
export type LogLevel = (typeof logLevels)[number]
|
|
3
|
+
|
|
4
|
+
export class Logger {
|
|
5
|
+
private level: LogLevel
|
|
6
|
+
|
|
7
|
+
constructor(level: LogLevel = 'none') {
|
|
8
|
+
this.level = level
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
setLevel(level: LogLevel) {
|
|
12
|
+
this.level = level
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
coerceLevel(level: string): LogLevel {
|
|
16
|
+
if (logLevels.includes(level.toLocaleUpperCase() as LogLevel)) {
|
|
17
|
+
return level.toLocaleUpperCase() as LogLevel
|
|
18
|
+
}
|
|
19
|
+
return 'info'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
log(level: string, message: string, ...args: any[]) {
|
|
23
|
+
const logLevel = this.coerceLevel(level)
|
|
24
|
+
if (!this.shouldLog(logLevel)) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
const consoleFn = console[logLevel]
|
|
28
|
+
consoleFn(`[${logLevel.toUpperCase()}] ${message}`, ...args)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
debug(message: string, ...args: any[]) {
|
|
32
|
+
this.log('debug', message, ...args)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
info(message: string, ...args: any[]) {
|
|
36
|
+
this.log('info', message, ...args)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
warn(message: string, ...args: any[]) {
|
|
40
|
+
this.log('warn', message, ...args)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
error(message: string, ...args: any[]) {
|
|
44
|
+
this.log('error', message, ...args)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private shouldLog(
|
|
48
|
+
messageLevel: LogLevel,
|
|
49
|
+
): messageLevel is Exclude<LogLevel, 'none'> {
|
|
50
|
+
const levels: LogLevel[] = ['debug', 'info', 'warn', 'error', 'none']
|
|
51
|
+
const messageLevelIndex = levels.indexOf(messageLevel)
|
|
52
|
+
const currentLevelIndex = levels.indexOf(this.level)
|
|
53
|
+
return (
|
|
54
|
+
currentLevelIndex <= messageLevelIndex &&
|
|
55
|
+
this.level !== 'none' &&
|
|
56
|
+
messageLevel !== 'none'
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const logger = new Logger()
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CancelFunction,
|
|
3
|
+
JSONValue,
|
|
4
|
+
ModuleKind,
|
|
5
|
+
StreamError,
|
|
6
|
+
StreamResult,
|
|
7
|
+
WorkerMessageType,
|
|
8
|
+
} from '../types'
|
|
9
|
+
import { logger } from '../utils/logger'
|
|
10
|
+
|
|
11
|
+
// Handles communication with the wasm worker
|
|
12
|
+
// TODO: Move rpc stream management to a separate "SubscriptionManager" class
|
|
13
|
+
export class WorkerClient {
|
|
14
|
+
private worker: Worker
|
|
15
|
+
private requestCounter = 0
|
|
16
|
+
private requestCallbacks = new Map<number, (value: any) => void>()
|
|
17
|
+
private initPromise: Promise<boolean> | undefined = undefined
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
// Must create the URL inside the constructor for vite
|
|
21
|
+
this.worker = new Worker(new URL('./worker.js', import.meta.url), {
|
|
22
|
+
type: 'module',
|
|
23
|
+
})
|
|
24
|
+
this.worker.onmessage = this.handleWorkerMessage.bind(this)
|
|
25
|
+
this.worker.onerror = this.handleWorkerError.bind(this)
|
|
26
|
+
logger.info('WorkerClient instantiated')
|
|
27
|
+
logger.debug('WorkerClient', this.worker)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Idempotent setup - Loads the wasm module
|
|
31
|
+
initialize() {
|
|
32
|
+
if (this.initPromise) return this.initPromise
|
|
33
|
+
this.initPromise = this.sendSingleMessage('init')
|
|
34
|
+
return this.initPromise
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private handleWorkerLogs(event: MessageEvent) {
|
|
38
|
+
const { type, level, message, ...data } = event.data
|
|
39
|
+
logger.log(level, message, ...data)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private handleWorkerError(event: ErrorEvent) {
|
|
43
|
+
logger.error('Worker error', event)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private handleWorkerMessage(event: MessageEvent) {
|
|
47
|
+
const { type, requestId, ...data } = event.data
|
|
48
|
+
if (type === 'log') {
|
|
49
|
+
this.handleWorkerLogs(event.data)
|
|
50
|
+
}
|
|
51
|
+
const streamCallback = this.requestCallbacks.get(requestId)
|
|
52
|
+
// TODO: Handle errors... maybe have another callbacks list for errors?
|
|
53
|
+
logger.debug('WorkerClient - handleWorkerMessage', event.data)
|
|
54
|
+
if (streamCallback) {
|
|
55
|
+
streamCallback(data) // {data: something} OR {error: something}
|
|
56
|
+
} else {
|
|
57
|
+
logger.warn(
|
|
58
|
+
'WorkerClient - handleWorkerMessage - received message with no callback',
|
|
59
|
+
requestId,
|
|
60
|
+
event.data,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// TODO: Handle errors... maybe have another callbacks list for errors?
|
|
66
|
+
// TODO: Handle timeouts
|
|
67
|
+
// TODO: Handle multiple errors
|
|
68
|
+
|
|
69
|
+
sendSingleMessage<
|
|
70
|
+
Response extends JSONValue = JSONValue,
|
|
71
|
+
Payload extends JSONValue = JSONValue,
|
|
72
|
+
>(type: WorkerMessageType, payload?: Payload) {
|
|
73
|
+
return new Promise<Response>((resolve, reject) => {
|
|
74
|
+
const requestId = ++this.requestCounter
|
|
75
|
+
logger.debug('WorkerClient - sendSingleMessage', requestId, type, payload)
|
|
76
|
+
this.requestCallbacks.set(
|
|
77
|
+
requestId,
|
|
78
|
+
(response: StreamResult<Response>) => {
|
|
79
|
+
this.requestCallbacks.delete(requestId)
|
|
80
|
+
logger.debug(
|
|
81
|
+
'WorkerClient - sendSingleMessage - response',
|
|
82
|
+
requestId,
|
|
83
|
+
response,
|
|
84
|
+
)
|
|
85
|
+
if (response.data) resolve(response.data)
|
|
86
|
+
else if (response.error) reject(response.error)
|
|
87
|
+
else
|
|
88
|
+
logger.warn(
|
|
89
|
+
'WorkerClient - sendSingleMessage - malformed response',
|
|
90
|
+
requestId,
|
|
91
|
+
response,
|
|
92
|
+
)
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
this.worker.postMessage({ type, payload, requestId })
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @summary Initiates an RPC stream with the specified module and method.
|
|
101
|
+
*
|
|
102
|
+
* @description
|
|
103
|
+
* This function sets up an RPC stream by sending a request to a worker and
|
|
104
|
+
* handling responses asynchronously. It ensures that unsubscription is handled
|
|
105
|
+
* correctly, even if the unsubscribe function is called before the subscription
|
|
106
|
+
* is fully established, by deferring the unsubscription attempt using `setTimeout`.
|
|
107
|
+
*
|
|
108
|
+
* The function operates in a non-blocking manner, leveraging Promises to manage
|
|
109
|
+
* asynchronous operations and callbacks to handle responses.
|
|
110
|
+
*
|
|
111
|
+
*
|
|
112
|
+
* @template Response - The expected type of the successful response.
|
|
113
|
+
* @template Body - The type of the request body.
|
|
114
|
+
* @param module - The module kind to interact with.
|
|
115
|
+
* @param method - The method name to invoke on the module.
|
|
116
|
+
* @param body - The request payload.
|
|
117
|
+
* @param onSuccess - Callback invoked with the response data on success.
|
|
118
|
+
* @param onError - Callback invoked with error information if an error occurs.
|
|
119
|
+
* @param onEnd - Optional callback invoked when the stream ends.
|
|
120
|
+
* @returns A function that can be called to cancel the subscription.
|
|
121
|
+
*
|
|
122
|
+
*/
|
|
123
|
+
rpcStream<
|
|
124
|
+
Response extends JSONValue = JSONValue,
|
|
125
|
+
Body extends JSONValue = JSONValue,
|
|
126
|
+
>(
|
|
127
|
+
module: ModuleKind,
|
|
128
|
+
method: string,
|
|
129
|
+
body: Body,
|
|
130
|
+
onSuccess: (res: Response) => void,
|
|
131
|
+
onError: (res: StreamError['error']) => void,
|
|
132
|
+
onEnd: () => void = () => {},
|
|
133
|
+
): CancelFunction {
|
|
134
|
+
const requestId = ++this.requestCounter
|
|
135
|
+
logger.debug('WorkerClient - rpcStream', requestId, module, method, body)
|
|
136
|
+
let unsubscribe: (value: void) => void = () => {}
|
|
137
|
+
let isSubscribed = false
|
|
138
|
+
|
|
139
|
+
const unsubscribePromise = new Promise<void>((resolve) => {
|
|
140
|
+
unsubscribe = () => {
|
|
141
|
+
if (isSubscribed) {
|
|
142
|
+
// If already subscribed, resolve immediately to trigger unsubscription
|
|
143
|
+
resolve()
|
|
144
|
+
} else {
|
|
145
|
+
// If not yet subscribed, defer the unsubscribe attempt to the next event loop tick
|
|
146
|
+
// This ensures that subscription setup has time to complete
|
|
147
|
+
setTimeout(() => unsubscribe(), 0)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// Initiate the inner RPC stream setup asynchronously
|
|
153
|
+
this._rpcStreamInner(
|
|
154
|
+
requestId,
|
|
155
|
+
module,
|
|
156
|
+
method,
|
|
157
|
+
body,
|
|
158
|
+
onSuccess,
|
|
159
|
+
onError,
|
|
160
|
+
onEnd,
|
|
161
|
+
unsubscribePromise,
|
|
162
|
+
).then(() => {
|
|
163
|
+
isSubscribed = true
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
return unsubscribe
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private async _rpcStreamInner<
|
|
170
|
+
Response extends JSONValue = JSONValue,
|
|
171
|
+
Body extends JSONValue = JSONValue,
|
|
172
|
+
>(
|
|
173
|
+
requestId: number,
|
|
174
|
+
module: ModuleKind,
|
|
175
|
+
method: string,
|
|
176
|
+
body: Body,
|
|
177
|
+
onSuccess: (res: Response) => void,
|
|
178
|
+
onError: (res: StreamError['error']) => void,
|
|
179
|
+
onEnd: () => void = () => {},
|
|
180
|
+
unsubscribePromise: Promise<void>,
|
|
181
|
+
// Unsubscribe function
|
|
182
|
+
) {
|
|
183
|
+
// await this.openPromise
|
|
184
|
+
// if (!this.worker || !this._isOpen)
|
|
185
|
+
// throw new Error('FedimintWallet is not open')
|
|
186
|
+
|
|
187
|
+
this.requestCallbacks.set(requestId, (response: StreamResult<Response>) => {
|
|
188
|
+
if (response.error !== undefined) {
|
|
189
|
+
onError(response.error)
|
|
190
|
+
} else if (response.data !== undefined) {
|
|
191
|
+
onSuccess(response.data)
|
|
192
|
+
} else if (response.end !== undefined) {
|
|
193
|
+
this.requestCallbacks.delete(requestId)
|
|
194
|
+
onEnd()
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
this.worker.postMessage({
|
|
198
|
+
type: 'rpc',
|
|
199
|
+
payload: { module, method, body },
|
|
200
|
+
requestId,
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
unsubscribePromise.then(() => {
|
|
204
|
+
this.worker?.postMessage({
|
|
205
|
+
type: 'unsubscribe',
|
|
206
|
+
requestId,
|
|
207
|
+
})
|
|
208
|
+
this.requestCallbacks.delete(requestId)
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
rpcSingle<
|
|
213
|
+
Response extends JSONValue = JSONValue,
|
|
214
|
+
Error extends string = string,
|
|
215
|
+
>(module: ModuleKind, method: string, body: JSONValue) {
|
|
216
|
+
logger.debug('WorkerClient - rpcSingle', module, method, body)
|
|
217
|
+
return new Promise<Response>((resolve, reject) => {
|
|
218
|
+
this.rpcStream<Response>(module, method, body, resolve, reject)
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async cleanup() {
|
|
223
|
+
await this.sendSingleMessage('cleanup')
|
|
224
|
+
this.requestCounter = 0
|
|
225
|
+
this.initPromise = undefined
|
|
226
|
+
this.requestCallbacks.clear()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// For Testing
|
|
230
|
+
_getRequestCounter() {
|
|
231
|
+
return this.requestCounter
|
|
232
|
+
}
|
|
233
|
+
_getRequestCallbackMap() {
|
|
234
|
+
return this.requestCallbacks
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { WorkerClient } from './WorkerClient'
|