Skip to content

Commit b4d6fa1

Browse files
committed
Add instance-based API: createInstance() → MotelyInstance
MotelyWasmExports is now a static dispatch layer over MotelyInstance objects. Each instance holds its own search state (CTS, cancellation). Multiple instances can run concurrently. JS API: loadMotely() → runtime with createInstance(). Instance has search, analyze, stop, destroy. Old flat methods still work via a default instance for backward compat. C# side: MotelyInstance.cs manages lifecycle via int handles (because [JSExport] only works on static methods). Static exports route through instance ID. https://claude.ai/code/session_019weRHQbABp2dzatqkp2aKV
1 parent 9440f4c commit b4d6fa1

File tree

4 files changed

+251
-101
lines changed

4 files changed

+251
-101
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
namespace Motely.BrowserWasm;
2+
3+
/// <summary>
4+
/// A single Motely runtime instance. Holds its own search state and cancellation.
5+
/// Multiple instances can run concurrently (e.g. one searching, one analyzing).
6+
/// </summary>
7+
internal sealed class MotelyInstance : IDisposable
8+
{
9+
private static int _nextId;
10+
private static readonly Dictionary<int, MotelyInstance> _instances = new();
11+
12+
public int Id { get; }
13+
private CancellationTokenSource? _activeCts;
14+
private bool _disposed;
15+
16+
private MotelyInstance(int id) => Id = id;
17+
18+
internal static int Create()
19+
{
20+
var id = Interlocked.Increment(ref _nextId);
21+
var instance = new MotelyInstance(id);
22+
lock (_instances) _instances[id] = instance;
23+
return id;
24+
}
25+
26+
internal static MotelyInstance Get(int id)
27+
{
28+
lock (_instances)
29+
{
30+
if (_instances.TryGetValue(id, out var inst))
31+
return inst;
32+
}
33+
throw new InvalidOperationException($"No instance with id {id}.");
34+
}
35+
36+
internal static void Destroy(int id)
37+
{
38+
MotelyInstance? inst;
39+
lock (_instances)
40+
{
41+
if (!_instances.Remove(id, out inst))
42+
return;
43+
}
44+
inst.Dispose();
45+
}
46+
47+
internal bool IsSearchActive => _activeCts != null;
48+
49+
internal CancellationToken BeginSearch()
50+
{
51+
if (_activeCts != null)
52+
throw new InvalidOperationException("Search already running on this instance.");
53+
54+
var cts = new CancellationTokenSource();
55+
_activeCts = cts;
56+
return cts.Token;
57+
}
58+
59+
internal void EndSearch()
60+
{
61+
_activeCts = null;
62+
}
63+
64+
internal void CancelSearch()
65+
{
66+
try { _activeCts?.Cancel(); } catch { }
67+
}
68+
69+
public void Dispose()
70+
{
71+
if (_disposed) return;
72+
_disposed = true;
73+
CancelSearch();
74+
_activeCts?.Dispose();
75+
_activeCts = null;
76+
}
77+
}

Motely.BrowserWasm/MotelyWasmExports.cs

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,30 @@
99

1010
namespace Motely.BrowserWasm;
1111

12+
/// <summary>
13+
/// Static [JSExport] dispatch layer. Instance methods route through an int handle.
14+
/// JS callers: createInstance() → id, then pass id to search/analyze/stop/destroy.
15+
/// </summary>
1216
[SupportedOSPlatform("browser")]
1317
public static partial class MotelyWasmExports
1418
{
15-
private static CancellationTokenSource? _activeCts;
19+
// ── Instance lifecycle ──
1620

17-
// ── SEARCH ──
21+
[JSExport]
22+
public static int CreateInstance() => MotelyInstance.Create();
23+
24+
[JSExport]
25+
public static void DestroyInstance(int id) => MotelyInstance.Destroy(id);
26+
27+
// ── Search (all instance-scoped) ──
1828

1929
[JSExport]
2030
public static Task<string> StartJamlSearch(
21-
string jamlContent, int threadCount, int batchCharCount,
31+
int instanceId, string jamlContent, int threadCount, int batchCharCount,
2232
int startBatch, int endBatch,
2333
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number, JSType.Number>>] Action<long, long, long> onProgress,
2434
[JSMarshalAs<JSType.Function<JSType.String, JSType.Number>>] Action<string, int> onResult)
25-
=> RunSearch(jamlContent, new MotelySearchRequest
35+
=> RunSearch(instanceId, jamlContent, new MotelySearchRequest
2636
{
2737
ThreadCount = ResolveThreads(threadCount),
2838
BatchCharCount = ResolveBatch(batchCharCount),
@@ -32,11 +42,11 @@ public static Task<string> StartJamlSearch(
3242

3343
[JSExport]
3444
public static Task<string> StartSeedListSearch(
35-
string jamlContent, int threadCount, int batchCharCount,
45+
int instanceId, string jamlContent, int threadCount, int batchCharCount,
3646
[JSMarshalAs<JSType.Array<JSType.String>>] string[] seeds,
3747
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number, JSType.Number>>] Action<long, long, long> onProgress,
3848
[JSMarshalAs<JSType.Function<JSType.String, JSType.Number>>] Action<string, int> onResult)
39-
=> RunSearch(jamlContent, new MotelySearchRequest
49+
=> RunSearch(instanceId, jamlContent, new MotelySearchRequest
4050
{
4151
ThreadCount = ResolveThreads(threadCount),
4252
BatchCharCount = ResolveBatch(batchCharCount),
@@ -45,11 +55,11 @@ public static Task<string> StartSeedListSearch(
4555

4656
[JSExport]
4757
public static Task<string> StartKeywordSearch(
48-
string jamlContent, int threadCount, int batchCharCount,
58+
int instanceId, string jamlContent, int threadCount, int batchCharCount,
4959
[JSMarshalAs<JSType.Array<JSType.String>>] string[] keywords, string padding,
5060
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number, JSType.Number>>] Action<long, long, long> onProgress,
5161
[JSMarshalAs<JSType.Function<JSType.String, JSType.Number>>] Action<string, int> onResult)
52-
=> RunSearch(jamlContent, new MotelySearchRequest
62+
=> RunSearch(instanceId, jamlContent, new MotelySearchRequest
5363
{
5464
ThreadCount = ResolveThreads(threadCount),
5565
BatchCharCount = ResolveBatch(batchCharCount),
@@ -59,10 +69,10 @@ public static Task<string> StartKeywordSearch(
5969

6070
[JSExport]
6171
public static Task<string> StartRandomSearch(
62-
string jamlContent, int threadCount, int batchCharCount, int count,
72+
int instanceId, string jamlContent, int threadCount, int batchCharCount, int count,
6373
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number, JSType.Number>>] Action<long, long, long> onProgress,
6474
[JSMarshalAs<JSType.Function<JSType.String, JSType.Number>>] Action<string, int> onResult)
65-
=> RunSearch(jamlContent, new MotelySearchRequest
75+
=> RunSearch(instanceId, jamlContent, new MotelySearchRequest
6676
{
6777
ThreadCount = ResolveThreads(threadCount),
6878
BatchCharCount = ResolveBatch(batchCharCount),
@@ -71,28 +81,29 @@ public static Task<string> StartRandomSearch(
7181

7282
[JSExport]
7383
public static Task<string> StartPalindromeSearch(
74-
string jamlContent, int threadCount, int batchCharCount,
84+
int instanceId, string jamlContent, int threadCount, int batchCharCount,
7585
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number, JSType.Number>>] Action<long, long, long> onProgress,
7686
[JSMarshalAs<JSType.Function<JSType.String, JSType.Number>>] Action<string, int> onResult)
77-
=> RunSearch(jamlContent, new MotelySearchRequest
87+
=> RunSearch(instanceId, jamlContent, new MotelySearchRequest
7888
{
7989
ThreadCount = ResolveThreads(threadCount),
8090
BatchCharCount = ResolveBatch(batchCharCount),
8191
Palindrome = true,
8292
}, onProgress, onResult);
8393

8494
[JSExport]
85-
public static Task StopSearch()
95+
public static Task StopSearch(int instanceId)
8696
{
87-
try { _activeCts?.Cancel(); } catch { }
97+
MotelyInstance.Get(instanceId).CancelSearch();
8898
return Task.CompletedTask;
8999
}
90100

91-
// ── ANALYZE ──
101+
// ── Analyze (instance-scoped for future streaming) ──
92102

93103
[JSExport]
94-
public static Task<string> AnalyzeSeed(string seed, string deck, string stake)
104+
public static Task<string> AnalyzeSeed(int instanceId, string seed, string deck, string stake)
95105
{
106+
// instanceId reserved for future per-instance state (streaming analysis, caching)
96107
try
97108
{
98109
var dto = MotelySeedAnalyzer.AnalyzeToDto(seed, deck, stake);
@@ -106,7 +117,7 @@ public static Task<string> AnalyzeSeed(string seed, string deck, string stake)
106117
}
107118
}
108119

109-
// ── GETTERS ──
120+
// ── Global (no instance needed) ──
110121

111122
[JSExport]
112123
public static Task<string> GetVersion() => Task.FromResult(MotelyBuildVersion.For(typeof(MotelyCore).Assembly));
@@ -134,23 +145,22 @@ public static Task<string> ValidateJamlWithError(string jamlContent)
134145
private static int ResolveBatch(int b) => b is >= 1 and <= 7 ? b : 4;
135146

136147
private static async Task<string> RunSearch(
137-
string jamlContent, MotelySearchRequest request,
148+
int instanceId, string jamlContent, MotelySearchRequest request,
138149
Action<long, long, long> onProgress, Action<string, int> onResult)
139150
{
140-
if (_activeCts != null)
141-
return "error: search already running";
142-
143-
var cts = new CancellationTokenSource();
144-
_activeCts = cts;
151+
var instance = MotelyInstance.Get(instanceId);
152+
if (instance.IsSearchActive)
153+
return "error: search already running on this instance";
145154

155+
var token = instance.BeginSearch();
146156
try
147157
{
148158
var (status, _, _) = await Task.Run(() =>
149-
MotelySearchOrchestrator.RunSearch(jamlContent, request, onProgress, onResult, cts.Token));
159+
MotelySearchOrchestrator.RunSearch(jamlContent, request, onProgress, onResult, token));
150160
return status;
151161
}
152162
catch (OperationCanceledException) { return "cancelled"; }
153163
catch (Exception ex) { return $"error: {ex.Message}"; }
154-
finally { _activeCts = null; }
164+
finally { instance.EndSearch(); }
155165
}
156166
}

motely-wasm/index.d.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,25 +68,48 @@ export interface CapabilitiesInfo {
6868
timestamp: string;
6969
}
7070

71-
// ── API ──
71+
// ── Instance ──
72+
73+
export interface MotelyInstance {
74+
readonly id: number;
75+
readonly isDestroyed: boolean;
76+
77+
analyzeSeed(seed: string, deck: string, stake: string): Promise<SeedAnalysisInfo>;
78+
79+
startJamlSearch(jamlContent: string, options?: SequentialSearchOptions): Promise<string>;
80+
verifySeed(jamlContent: string, seed: string, options?: SearchRuntimeOptions): Promise<string>;
81+
startSeedListSearch(jamlContent: string, seeds: string[], options?: SearchRuntimeOptions): Promise<string>;
82+
startKeywordSearch(jamlContent: string, keyword: string, options?: KeywordSearchOptions): Promise<string>;
83+
startKeywordsSearch(jamlContent: string, keywords: string[], options?: KeywordSearchOptions): Promise<string>;
84+
startRandomSearch(jamlContent: string, count: number, options?: SearchRuntimeOptions): Promise<string>;
85+
startPalindromeSearch(jamlContent: string, options?: SearchRuntimeOptions): Promise<string>;
86+
stopSearch(): Promise<void>;
87+
88+
destroy(): void;
89+
}
90+
91+
// ── Runtime API ──
7292

7393
export interface MotelyWasmApi {
94+
createInstance(): MotelyInstance;
95+
96+
// Global (no instance needed)
7497
getVersion(): Promise<string>;
7598
getCapabilities(): Promise<CapabilitiesInfo>;
7699
isSimdEnabled(): Promise<boolean>;
77100
getProcessorCount(): Promise<number>;
78-
79-
analyzeSeed(seed: string, deck: string, stake: string): Promise<SeedAnalysisInfo>;
80101
validateJaml(jamlContent: string): Promise<ValidateResult>;
81102

103+
// Backward compat (uses default instance)
104+
analyzeSeed(seed: string, deck: string, stake: string): Promise<SeedAnalysisInfo>;
82105
startJamlSearch(jamlContent: string, options?: SequentialSearchOptions): Promise<string>;
83106
verifySeed(jamlContent: string, seed: string, options?: SearchRuntimeOptions): Promise<string>;
84107
startSeedListSearch(jamlContent: string, seeds: string[], options?: SearchRuntimeOptions): Promise<string>;
85108
startKeywordSearch(jamlContent: string, keyword: string, options?: KeywordSearchOptions): Promise<string>;
86109
startKeywordsSearch(jamlContent: string, keywords: string[], options?: KeywordSearchOptions): Promise<string>;
87110
startRandomSearch(jamlContent: string, count: number, options?: SearchRuntimeOptions): Promise<string>;
88111
startPalindromeSearch(jamlContent: string, options?: SearchRuntimeOptions): Promise<string>;
89-
stopSearch(): void;
112+
stopSearch(): Promise<void>;
90113
}
91114

92115
export interface LoadMotelyOptions {

0 commit comments

Comments
 (0)