@luxexchange/sessions 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.depcheckrc +20 -0
- package/.eslintrc.js +21 -0
- package/README.md +1 -0
- package/env.d.ts +12 -0
- package/package.json +50 -0
- package/project.json +42 -0
- package/src/challenge-solvers/createChallengeSolverService.ts +64 -0
- package/src/challenge-solvers/createHashcashMockSolver.ts +39 -0
- package/src/challenge-solvers/createHashcashSolver.test.ts +385 -0
- package/src/challenge-solvers/createHashcashSolver.ts +255 -0
- package/src/challenge-solvers/createNoneMockSolver.ts +11 -0
- package/src/challenge-solvers/createTurnstileMockSolver.ts +30 -0
- package/src/challenge-solvers/createTurnstileSolver.ts +353 -0
- package/src/challenge-solvers/hashcash/core.native.ts +34 -0
- package/src/challenge-solvers/hashcash/core.test.ts +314 -0
- package/src/challenge-solvers/hashcash/core.ts +35 -0
- package/src/challenge-solvers/hashcash/core.web.ts +123 -0
- package/src/challenge-solvers/hashcash/createWorkerHashcashSolver.test.ts +195 -0
- package/src/challenge-solvers/hashcash/createWorkerHashcashSolver.ts +120 -0
- package/src/challenge-solvers/hashcash/shared.ts +70 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel.native.ts +22 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel.ts +22 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel.web.ts +212 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel.native.ts +16 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel.ts +16 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel.web.ts +97 -0
- package/src/challenge-solvers/hashcash/worker/hashcash.worker.ts +91 -0
- package/src/challenge-solvers/hashcash/worker/types.ts +69 -0
- package/src/challenge-solvers/turnstileErrors.ts +49 -0
- package/src/challenge-solvers/turnstileScriptLoader.ts +325 -0
- package/src/challenge-solvers/turnstileSolver.integration.test.ts +331 -0
- package/src/challenge-solvers/types.ts +55 -0
- package/src/challengeFlow.integration.test.ts +627 -0
- package/src/device-id/createDeviceIdService.ts +19 -0
- package/src/device-id/types.ts +11 -0
- package/src/index.ts +137 -0
- package/src/lux-identifier/createLuxIdentifierService.ts +19 -0
- package/src/lux-identifier/luxIdentifierQuery.ts +20 -0
- package/src/lux-identifier/types.ts +11 -0
- package/src/oauth-service/createOAuthService.ts +125 -0
- package/src/oauth-service/types.ts +104 -0
- package/src/performance/createNoopPerformanceTracker.ts +12 -0
- package/src/performance/createPerformanceTracker.ts +43 -0
- package/src/performance/index.ts +7 -0
- package/src/performance/types.ts +11 -0
- package/src/session-initialization/createSessionInitializationService.test.ts +557 -0
- package/src/session-initialization/createSessionInitializationService.ts +193 -0
- package/src/session-initialization/sessionErrors.ts +32 -0
- package/src/session-repository/createSessionClient.ts +10 -0
- package/src/session-repository/createSessionRepository.test.ts +313 -0
- package/src/session-repository/createSessionRepository.ts +242 -0
- package/src/session-repository/errors.ts +22 -0
- package/src/session-repository/types.ts +289 -0
- package/src/session-service/createNoopSessionService.ts +24 -0
- package/src/session-service/createSessionService.test.ts +388 -0
- package/src/session-service/createSessionService.ts +61 -0
- package/src/session-service/types.ts +59 -0
- package/src/session-storage/createSessionStorage.ts +28 -0
- package/src/session-storage/types.ts +15 -0
- package/src/session.integration.test.ts +480 -0
- package/src/sessionLifecycle.integration.test.ts +264 -0
- package/src/test-utils/createLocalCookieTransport.ts +52 -0
- package/src/test-utils/createLocalHeaderTransport.ts +45 -0
- package/src/test-utils/mocks.ts +122 -0
- package/src/test-utils.ts +200 -0
- package/tsconfig.json +19 -0
- package/tsconfig.lint.json +8 -0
- package/tsconfig.spec.json +8 -0
- package/vitest.config.ts +20 -0
- package/vitest.integration.config.ts +14 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-backed hashcash solver.
|
|
3
|
+
*
|
|
4
|
+
* Offloads proof-of-work computation to a Web Worker to avoid
|
|
5
|
+
* blocking the main thread.
|
|
6
|
+
*/
|
|
7
|
+
import { parseHashcashChallenge } from '@luxexchange/sessions/src/challenge-solvers/createHashcashSolver'
|
|
8
|
+
import type { HashcashChallenge } from '@luxexchange/sessions/src/challenge-solvers/hashcash/core'
|
|
9
|
+
import type { HashcashWorkerChannelFactory } from '@luxexchange/sessions/src/challenge-solvers/hashcash/worker/types'
|
|
10
|
+
import type { ChallengeData, ChallengeSolver } from '@luxexchange/sessions/src/challenge-solvers/types'
|
|
11
|
+
|
|
12
|
+
interface CreateWorkerHashcashSolverContext {
|
|
13
|
+
/**
|
|
14
|
+
* Factory function to create a worker channel.
|
|
15
|
+
* Platform-specific (web, extension, native) implementations
|
|
16
|
+
* are injected here.
|
|
17
|
+
*/
|
|
18
|
+
createChannel: HashcashWorkerChannelFactory
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Optional AbortSignal for external cancellation.
|
|
22
|
+
* When aborted, cancels the in-progress proof search.
|
|
23
|
+
*/
|
|
24
|
+
signal?: AbortSignal
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates a hashcash solver that runs proof-of-work in a worker.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { createHashcashWorkerChannel } from './worker/createHashcashWorkerChannel.web'
|
|
33
|
+
*
|
|
34
|
+
* const getWorker = () => new Worker(new URL('./hashcash.worker.ts', import.meta.url), { type: 'module' })
|
|
35
|
+
*
|
|
36
|
+
* const solver = createWorkerHashcashSolver({
|
|
37
|
+
* createChannel: () => createHashcashWorkerChannel({ getWorker })
|
|
38
|
+
* })
|
|
39
|
+
*
|
|
40
|
+
* const solution = await solver.solve(challengeData)
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
function createWorkerHashcashSolver(ctx: CreateWorkerHashcashSolverContext): ChallengeSolver {
|
|
44
|
+
async function solve(challengeData: ChallengeData): Promise<string> {
|
|
45
|
+
let challenge: HashcashChallenge
|
|
46
|
+
|
|
47
|
+
// Prefer typed challengeData over legacy extra field
|
|
48
|
+
if (challengeData.challengeData?.case === 'hashcash') {
|
|
49
|
+
const typed = challengeData.challengeData.value
|
|
50
|
+
challenge = {
|
|
51
|
+
difficulty: typed.difficulty,
|
|
52
|
+
subject: typed.subject,
|
|
53
|
+
algorithm: typed.algorithm as 'sha256',
|
|
54
|
+
nonce: typed.nonce,
|
|
55
|
+
max_proof_length: typed.maxProofLength,
|
|
56
|
+
verifier: typed.verifier,
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
const challengeDataStr = challengeData.extra?.['challengeData']
|
|
60
|
+
if (!challengeDataStr) {
|
|
61
|
+
throw new Error('Missing challengeData in challenge extra field')
|
|
62
|
+
}
|
|
63
|
+
challenge = parseHashcashChallenge(challengeDataStr)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Create worker channel
|
|
67
|
+
const channel = ctx.createChannel()
|
|
68
|
+
|
|
69
|
+
// Handle external cancellation
|
|
70
|
+
const abortHandler = (): void => {
|
|
71
|
+
channel.api.cancel().catch(() => {
|
|
72
|
+
// Ignore cancellation errors
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (ctx.signal) {
|
|
77
|
+
ctx.signal.addEventListener('abort', abortHandler)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Check if already aborted
|
|
82
|
+
if (ctx.signal?.aborted) {
|
|
83
|
+
throw new Error('Challenge solving was cancelled')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Find proof-of-work solution in worker
|
|
87
|
+
const proof = await channel.api.findProof({
|
|
88
|
+
challenge,
|
|
89
|
+
rangeStart: 0,
|
|
90
|
+
rangeSize: challenge.max_proof_length,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
if (!proof) {
|
|
94
|
+
// Could be cancelled or no solution found
|
|
95
|
+
if (ctx.signal?.aborted) {
|
|
96
|
+
throw new Error('Challenge solving was cancelled')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Failed to find valid proof within allowed range (0-${challenge.max_proof_length}). ` +
|
|
101
|
+
'Challenge may have expired or difficulty may be too high.',
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Return the solution in the format expected by backend: "${subject}:${nonce}:${counter}"
|
|
106
|
+
return `${challenge.subject}:${challenge.nonce}:${proof.counter}`
|
|
107
|
+
} finally {
|
|
108
|
+
// Clean up
|
|
109
|
+
if (ctx.signal) {
|
|
110
|
+
ctx.signal.removeEventListener('abort', abortHandler)
|
|
111
|
+
}
|
|
112
|
+
channel.terminate()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { solve }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { createWorkerHashcashSolver }
|
|
120
|
+
export type { CreateWorkerHashcashSolverContext }
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types and platform-agnostic utilities for hashcash.
|
|
3
|
+
*
|
|
4
|
+
* This file contains code that has zero platform dependencies and is
|
|
5
|
+
* imported by all platform variants (core.ts, core.web.ts, core.native.ts).
|
|
6
|
+
*
|
|
7
|
+
* NOTE: This file intentionally does NOT follow the platform-split naming
|
|
8
|
+
* convention (no .web.ts/.native.ts variants) so it can be safely imported
|
|
9
|
+
* from platform-specific files without circular resolution.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { base64 } from '@scure/base'
|
|
13
|
+
|
|
14
|
+
export interface HashcashChallenge {
|
|
15
|
+
difficulty: number
|
|
16
|
+
subject: string
|
|
17
|
+
algorithm: 'sha256'
|
|
18
|
+
nonce: string
|
|
19
|
+
max_proof_length: number
|
|
20
|
+
verifier?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ProofResult {
|
|
24
|
+
counter: string
|
|
25
|
+
hash: Uint8Array
|
|
26
|
+
attempts: number
|
|
27
|
+
timeMs: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if a hash meets the required difficulty.
|
|
32
|
+
* Difficulty is the number of leading zero bytes required.
|
|
33
|
+
*/
|
|
34
|
+
export function checkDifficulty(hash: Uint8Array, difficulty: number): boolean {
|
|
35
|
+
// Backend uses difficulty as number of zero BYTES, not bits
|
|
36
|
+
// difficulty=1 means first byte must be 0
|
|
37
|
+
// difficulty=2 means first two bytes must be 0, etc.
|
|
38
|
+
|
|
39
|
+
// Check if hash has enough bytes
|
|
40
|
+
if (hash.length < difficulty) {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check that the first 'difficulty' bytes are all zero
|
|
45
|
+
for (let i = 0; i < difficulty; i++) {
|
|
46
|
+
if (hash[i] !== 0) {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Format a proof into hashcash string format for submission.
|
|
56
|
+
* Format: version:bits:date:resource:extension:counter:hash
|
|
57
|
+
*/
|
|
58
|
+
export function formatHashcashString(challenge: HashcashChallenge, proof: ProofResult): string {
|
|
59
|
+
const version = '1'
|
|
60
|
+
const bits = challenge.difficulty.toString()
|
|
61
|
+
const date = new Date().toISOString().slice(2, 10).replace(/-/g, '')
|
|
62
|
+
const resource = challenge.subject.slice(0, 16)
|
|
63
|
+
const extension = ''
|
|
64
|
+
const counter = proof.counter
|
|
65
|
+
|
|
66
|
+
// Encode the proof hash as base64 (truncated)
|
|
67
|
+
const hashB64 = base64.encode(proof.hash).slice(0, 27)
|
|
68
|
+
|
|
69
|
+
return `${version}:${bits}:${date}:${resource}:${extension}:${counter}:${hashB64}`
|
|
70
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native stub for multi-worker hashcash channel factory.
|
|
3
|
+
* Web Workers are not available in React Native.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { HashcashWorkerChannel } from '@luxexchange/sessions/src/challenge-solvers/hashcash/worker/types'
|
|
7
|
+
import { NotImplementedError } from '@luxfi/utilities/src/errors'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Configuration for multi-worker hashcash channel.
|
|
11
|
+
*/
|
|
12
|
+
interface MultiWorkerConfig {
|
|
13
|
+
workerCount?: number
|
|
14
|
+
getWorker: () => Worker
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createHashcashMultiWorkerChannel(_config: MultiWorkerConfig): HashcashWorkerChannel {
|
|
18
|
+
throw new NotImplementedError('createHashcashMultiWorkerChannel')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { createHashcashMultiWorkerChannel }
|
|
22
|
+
export type { MultiWorkerConfig }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base stub for multi-worker hashcash channel factory.
|
|
3
|
+
* Platform-specific implementations override this file.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { HashcashWorkerChannel } from '@luxexchange/sessions/src/challenge-solvers/hashcash/worker/types'
|
|
7
|
+
import { PlatformSplitStubError } from '@luxfi/utilities/src/errors'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Configuration for multi-worker hashcash channel.
|
|
11
|
+
*/
|
|
12
|
+
interface MultiWorkerConfig {
|
|
13
|
+
workerCount?: number
|
|
14
|
+
getWorker: () => Worker
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createHashcashMultiWorkerChannel(_config: MultiWorkerConfig): HashcashWorkerChannel {
|
|
18
|
+
throw new PlatformSplitStubError('createHashcashMultiWorkerChannel')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { createHashcashMultiWorkerChannel }
|
|
22
|
+
export type { MultiWorkerConfig }
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-worker channel factory for parallel hashcash proof-of-work.
|
|
3
|
+
*
|
|
4
|
+
* Spawns multiple Web Workers to search different counter ranges in parallel.
|
|
5
|
+
* Uses Promise.race() - first worker to find a valid proof wins.
|
|
6
|
+
*
|
|
7
|
+
* This provides significant speedup on multi-core systems:
|
|
8
|
+
* - 4 workers ~= 4x speedup
|
|
9
|
+
* - 8 workers ~= 8x speedup (diminishing returns beyond core count)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ProofResult } from '@luxexchange/sessions/src/challenge-solvers/hashcash/core'
|
|
13
|
+
import type {
|
|
14
|
+
FindProofParams,
|
|
15
|
+
HashcashWorkerAPI,
|
|
16
|
+
HashcashWorkerChannel,
|
|
17
|
+
} from '@luxexchange/sessions/src/challenge-solvers/hashcash/worker/types'
|
|
18
|
+
import { createChannel } from 'bidc'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Configuration for multi-worker hashcash channel.
|
|
22
|
+
*/
|
|
23
|
+
interface MultiWorkerConfig {
|
|
24
|
+
/**
|
|
25
|
+
* Number of workers to spawn.
|
|
26
|
+
* Defaults to navigator.hardwareConcurrency or 4.
|
|
27
|
+
*/
|
|
28
|
+
workerCount?: number
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Factory function to create a Worker instance.
|
|
32
|
+
*/
|
|
33
|
+
getWorker: () => Worker
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Internal worker state for tracking individual workers.
|
|
38
|
+
*/
|
|
39
|
+
interface WorkerState {
|
|
40
|
+
worker: Worker
|
|
41
|
+
channel: ReturnType<typeof createChannel>
|
|
42
|
+
cancelled: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Creates a multi-worker channel for parallel hashcash proof-of-work.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* const channel = createHashcashMultiWorkerChannel({
|
|
51
|
+
* workerCount: 4,
|
|
52
|
+
* getWorker: () => new Worker(
|
|
53
|
+
* new URL('./hashcash.worker.ts', import.meta.url),
|
|
54
|
+
* { type: 'module' }
|
|
55
|
+
* ),
|
|
56
|
+
* })
|
|
57
|
+
*
|
|
58
|
+
* const proof = await channel.api.findProof({ challenge })
|
|
59
|
+
* channel.terminate()
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
function createHashcashMultiWorkerChannel(config: MultiWorkerConfig): HashcashWorkerChannel {
|
|
63
|
+
const workerCount = config.workerCount ?? (typeof navigator !== 'undefined' ? navigator.hardwareConcurrency : 4)
|
|
64
|
+
const workers: WorkerState[] = []
|
|
65
|
+
|
|
66
|
+
// Track if we've been terminated
|
|
67
|
+
let terminated = false
|
|
68
|
+
|
|
69
|
+
// Initialize workers lazily on first findProof call
|
|
70
|
+
const initWorkers = (): void => {
|
|
71
|
+
if (workers.length > 0 || terminated) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < workerCount; i++) {
|
|
76
|
+
const worker = config.getWorker()
|
|
77
|
+
const channel = createChannel(worker)
|
|
78
|
+
workers.push({ worker, channel, cancelled: false })
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const api: HashcashWorkerAPI = {
|
|
83
|
+
async findProof(params: FindProofParams): Promise<ProofResult | null> {
|
|
84
|
+
if (terminated) {
|
|
85
|
+
throw new Error('Multi-worker channel has been terminated')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
initWorkers()
|
|
89
|
+
|
|
90
|
+
// Reset cancelled state for all workers at start of new search
|
|
91
|
+
workers.forEach((state) => {
|
|
92
|
+
state.cancelled = false
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const { challenge, rangeStart = 0, rangeSize = challenge.max_proof_length } = params
|
|
96
|
+
const rangeEnd = rangeStart + rangeSize
|
|
97
|
+
|
|
98
|
+
// Divide range across workers
|
|
99
|
+
const rangePerWorker = Math.ceil(rangeSize / workerCount)
|
|
100
|
+
|
|
101
|
+
// Track completion state
|
|
102
|
+
let foundResult: ProofResult | null = null
|
|
103
|
+
let completedCount = 0
|
|
104
|
+
|
|
105
|
+
// Pre-calculate expected worker count before starting any workers.
|
|
106
|
+
// This avoids a subtle dependency on JS event loop microtask ordering:
|
|
107
|
+
// if we counted inline, a fast-completing worker's .then() could
|
|
108
|
+
// theoretically race with the loop incrementing the started count.
|
|
109
|
+
let expectedCount = 0
|
|
110
|
+
workers.forEach((state, index) => {
|
|
111
|
+
if (state.cancelled || terminated) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
const workerRangeStart = rangeStart + index * rangePerWorker
|
|
115
|
+
const workerRangeEnd = Math.min(workerRangeStart + rangePerWorker, rangeEnd)
|
|
116
|
+
if (workerRangeEnd - workerRangeStart > 0) {
|
|
117
|
+
expectedCount++
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
if (expectedCount === 0) {
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return new Promise((resolve) => {
|
|
126
|
+
// Start all workers in parallel
|
|
127
|
+
workers.forEach((state, index) => {
|
|
128
|
+
if (terminated) {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const workerRangeStart = rangeStart + index * rangePerWorker
|
|
133
|
+
const workerRangeEnd = Math.min(workerRangeStart + rangePerWorker, rangeEnd)
|
|
134
|
+
const workerRangeSize = workerRangeEnd - workerRangeStart
|
|
135
|
+
|
|
136
|
+
if (workerRangeSize <= 0) {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Start worker search (don't await - let them race)
|
|
141
|
+
state.channel
|
|
142
|
+
.send({
|
|
143
|
+
type: 'findProof',
|
|
144
|
+
params: {
|
|
145
|
+
challenge,
|
|
146
|
+
rangeStart: workerRangeStart,
|
|
147
|
+
rangeSize: workerRangeSize,
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
.then((result: unknown) => {
|
|
151
|
+
// Check if worker returned busy response
|
|
152
|
+
if (result && typeof result === 'object' && 'busy' in result) {
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
return result as ProofResult | null
|
|
156
|
+
})
|
|
157
|
+
.then((result) => {
|
|
158
|
+
completedCount++
|
|
159
|
+
|
|
160
|
+
// First worker to find a valid proof wins
|
|
161
|
+
if (result && !foundResult && !terminated) {
|
|
162
|
+
foundResult = result
|
|
163
|
+
// Cancel all other workers immediately
|
|
164
|
+
api.cancel().catch(() => {})
|
|
165
|
+
resolve(result)
|
|
166
|
+
} else if (completedCount === expectedCount && !foundResult) {
|
|
167
|
+
// All expected workers done, no result found
|
|
168
|
+
resolve(null)
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
.catch(() => {
|
|
172
|
+
completedCount++
|
|
173
|
+
if (completedCount === expectedCount && !foundResult) {
|
|
174
|
+
resolve(null)
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
async cancel(): Promise<void> {
|
|
182
|
+
// Cancel all workers - fire and forget for speed
|
|
183
|
+
workers.forEach((state) => {
|
|
184
|
+
if (!state.cancelled) {
|
|
185
|
+
state.cancelled = true
|
|
186
|
+
state.channel.send({ type: 'cancel' }).catch(() => {
|
|
187
|
+
// Ignore cancel errors
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
api,
|
|
196
|
+
terminate(): void {
|
|
197
|
+
terminated = true
|
|
198
|
+
|
|
199
|
+
// Terminate all workers
|
|
200
|
+
for (const state of workers) {
|
|
201
|
+
state.cancelled = true
|
|
202
|
+
state.channel.cleanup()
|
|
203
|
+
state.worker.terminate()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
workers.length = 0
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export { createHashcashMultiWorkerChannel }
|
|
212
|
+
export type { MultiWorkerConfig }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native implementation of hashcash worker channel factory.
|
|
3
|
+
* Web Workers are not available in React Native.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
CreateHashcashWorkerChannelContext,
|
|
8
|
+
HashcashWorkerChannel,
|
|
9
|
+
} from '@luxexchange/sessions/src/challenge-solvers/hashcash/worker/types'
|
|
10
|
+
import { NotImplementedError } from '@luxfi/utilities/src/errors'
|
|
11
|
+
|
|
12
|
+
function createHashcashWorkerChannel(_ctx: CreateHashcashWorkerChannelContext): HashcashWorkerChannel {
|
|
13
|
+
throw new NotImplementedError('createHashcashWorkerChannel')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { createHashcashWorkerChannel }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base stub for hashcash worker channel factory.
|
|
3
|
+
* Platform-specific implementations override this file.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
CreateHashcashWorkerChannelContext,
|
|
8
|
+
HashcashWorkerChannel,
|
|
9
|
+
} from '@luxexchange/sessions/src/challenge-solvers/hashcash/worker/types'
|
|
10
|
+
import { PlatformSplitStubError } from '@luxfi/utilities/src/errors'
|
|
11
|
+
|
|
12
|
+
function createHashcashWorkerChannel(_ctx: CreateHashcashWorkerChannelContext): HashcashWorkerChannel {
|
|
13
|
+
throw new PlatformSplitStubError('createHashcashWorkerChannel')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { createHashcashWorkerChannel }
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Worker channel factory for hashcash proof-of-work.
|
|
3
|
+
*
|
|
4
|
+
* Creates a Web Worker and establishes a BIDC channel for
|
|
5
|
+
* bidirectional async communication.
|
|
6
|
+
*
|
|
7
|
+
* This is the web platform implementation used by both web app and extension.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ProofResult } from '@luxexchange/sessions/src/challenge-solvers/hashcash/core'
|
|
11
|
+
import type {
|
|
12
|
+
CreateHashcashWorkerChannelContext,
|
|
13
|
+
FindProofParams,
|
|
14
|
+
HashcashWorkerAPI,
|
|
15
|
+
HashcashWorkerChannel,
|
|
16
|
+
} from '@luxexchange/sessions/src/challenge-solvers/hashcash/worker/types'
|
|
17
|
+
import { createChannel } from 'bidc'
|
|
18
|
+
|
|
19
|
+
// Singleton worker instance for reuse
|
|
20
|
+
let sharedWorker: Worker | null = null
|
|
21
|
+
let sharedChannel: ReturnType<typeof createChannel> | null = null
|
|
22
|
+
let referenceCount = 0
|
|
23
|
+
// Track pending operations to reject on terminate
|
|
24
|
+
const pendingOperations = new Set<(err: Error) => void>()
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates (or reuses) a channel to the hashcash worker.
|
|
28
|
+
*
|
|
29
|
+
* Uses a shared worker instance to avoid creation overhead.
|
|
30
|
+
* The worker is only terminated when all channels are closed.
|
|
31
|
+
*
|
|
32
|
+
* @param ctx - Context containing getWorker function to create the Worker instance
|
|
33
|
+
*/
|
|
34
|
+
function createHashcashWorkerChannel(ctx: CreateHashcashWorkerChannelContext): HashcashWorkerChannel {
|
|
35
|
+
// Create worker on first use
|
|
36
|
+
if (!sharedWorker) {
|
|
37
|
+
sharedWorker = ctx.getWorker()
|
|
38
|
+
sharedChannel = createChannel(sharedWorker)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
referenceCount++
|
|
42
|
+
|
|
43
|
+
const channel = sharedChannel
|
|
44
|
+
if (!channel) {
|
|
45
|
+
throw new Error('Worker channel not initialized')
|
|
46
|
+
}
|
|
47
|
+
const { send } = channel
|
|
48
|
+
|
|
49
|
+
const api: HashcashWorkerAPI = {
|
|
50
|
+
findProof(params: FindProofParams): Promise<ProofResult | null> {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
pendingOperations.add(reject)
|
|
53
|
+
|
|
54
|
+
send({ type: 'findProof', params })
|
|
55
|
+
.then((result: unknown) => {
|
|
56
|
+
// Check if worker returned busy response
|
|
57
|
+
// Worker returns { busy: true } when another operation is in progress
|
|
58
|
+
if (result && typeof result === 'object' && 'busy' in result) {
|
|
59
|
+
reject(new Error('Worker is busy - another findProof operation is in progress'))
|
|
60
|
+
} else {
|
|
61
|
+
resolve(result as ProofResult | null)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
.catch(reject)
|
|
65
|
+
.finally(() => pendingOperations.delete(reject))
|
|
66
|
+
})
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async cancel(): Promise<void> {
|
|
70
|
+
await send({ type: 'cancel' })
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
api,
|
|
76
|
+
terminate(): void {
|
|
77
|
+
referenceCount--
|
|
78
|
+
|
|
79
|
+
// Only terminate when no more references
|
|
80
|
+
if (referenceCount <= 0 && sharedWorker) {
|
|
81
|
+
// Reject any pending operations before terminating
|
|
82
|
+
const error = new Error('Worker terminated while operation in progress')
|
|
83
|
+
for (const reject of pendingOperations) {
|
|
84
|
+
reject(error)
|
|
85
|
+
}
|
|
86
|
+
pendingOperations.clear()
|
|
87
|
+
|
|
88
|
+
sharedWorker.terminate()
|
|
89
|
+
sharedWorker = null
|
|
90
|
+
sharedChannel = null
|
|
91
|
+
referenceCount = 0
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export { createHashcashWorkerChannel }
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hashcash Web Worker
|
|
3
|
+
*
|
|
4
|
+
* Runs proof-of-work computation off the main thread using Web Crypto.
|
|
5
|
+
* Uses BIDC for bidirectional async communication.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ProofResult } from '../core'
|
|
9
|
+
import { findProof } from '../core'
|
|
10
|
+
import type { FindProofParams } from './types'
|
|
11
|
+
import type { SerializableValue } from 'bidc'
|
|
12
|
+
import { createChannel } from 'bidc'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Message types sent from main thread to worker
|
|
16
|
+
* These are plain objects at runtime, compatible with SerializableValue
|
|
17
|
+
*/
|
|
18
|
+
type WorkerMessage = { type: 'findProof'; params: FindProofParams } | { type: 'cancel' }
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Response type returned from worker to main thread.
|
|
22
|
+
* Matches ProofResult structure but explicitly typed for serialization.
|
|
23
|
+
*/
|
|
24
|
+
type WorkerResponse = ProofResult | null
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Operation state for single-operation-at-a-time enforcement.
|
|
28
|
+
*
|
|
29
|
+
* This worker is designed for single-operation-at-a-time usage. Concurrent findProof
|
|
30
|
+
* calls will be rejected with an error. This ensures clean cancellation semantics
|
|
31
|
+
* where cancel() only affects the one in-progress operation.
|
|
32
|
+
*/
|
|
33
|
+
let operationInProgress = false
|
|
34
|
+
let cancelled = false
|
|
35
|
+
|
|
36
|
+
// Create BIDC channel - in worker context, connects to parent
|
|
37
|
+
const { receive } = createChannel()
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Response type for busy state - signals to caller that worker is occupied.
|
|
41
|
+
* The channel layer converts this to an appropriate error.
|
|
42
|
+
*/
|
|
43
|
+
type WorkerBusyResponse = { busy: true }
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Async message handler for incoming requests from main thread.
|
|
47
|
+
* BIDC supports async handlers - it awaits the Promise before sending response.
|
|
48
|
+
*/
|
|
49
|
+
const messageHandler = async (data: WorkerMessage): Promise<WorkerResponse | WorkerBusyResponse> => {
|
|
50
|
+
switch (data.type) {
|
|
51
|
+
case 'findProof': {
|
|
52
|
+
// Enforce single-operation-at-a-time
|
|
53
|
+
if (operationInProgress) {
|
|
54
|
+
return { busy: true }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
operationInProgress = true
|
|
58
|
+
// Reset cancellation flag for new search
|
|
59
|
+
cancelled = false
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// findProof is now async (uses Web Crypto)
|
|
63
|
+
const result = await findProof({
|
|
64
|
+
challenge: data.params.challenge,
|
|
65
|
+
rangeStart: data.params.rangeStart,
|
|
66
|
+
rangeSize: data.params.rangeSize,
|
|
67
|
+
// Check cancellation flag during iteration
|
|
68
|
+
shouldStop: () => cancelled,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Return ProofResult directly - BIDC handles serialization of Uint8Array
|
|
72
|
+
return result
|
|
73
|
+
} finally {
|
|
74
|
+
operationInProgress = false
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case 'cancel': {
|
|
79
|
+
cancelled = true
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
default:
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Register async message handler with BIDC
|
|
89
|
+
// BIDC supports async handlers - the response Promise is awaited before sending
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
91
|
+
receive(messageHandler as (data: SerializableValue) => Promise<SerializableValue>)
|