@topgunbuild/client 0.7.0 → 0.8.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/dist/index.mjs CHANGED
@@ -709,6 +709,11 @@ var _SyncEngine = class _SyncEngine {
709
709
  // ============================================
710
710
  /** Message listeners for journal and other generic messages */
711
711
  this.messageListeners = /* @__PURE__ */ new Set();
712
+ // ============================================
713
+ // Full-Text Search Methods (Phase 11.1a)
714
+ // ============================================
715
+ /** Pending search requests by requestId */
716
+ this.pendingSearchRequests = /* @__PURE__ */ new Map();
712
717
  if (!config.serverUrl && !config.connectionProvider) {
713
718
  throw new Error("SyncEngine requires either serverUrl or connectionProvider");
714
719
  }
@@ -1594,6 +1599,21 @@ var _SyncEngine = class _SyncEngine {
1594
1599
  this.conflictResolverClient.handleMergeRejected(message);
1595
1600
  break;
1596
1601
  }
1602
+ // ============ Full-Text Search Message Handlers (Phase 11.1a) ============
1603
+ case "SEARCH_RESP": {
1604
+ logger.debug({ requestId: message.payload?.requestId, resultCount: message.payload?.results?.length }, "Received SEARCH_RESP");
1605
+ this.handleSearchResponse(message.payload);
1606
+ break;
1607
+ }
1608
+ // ============ Live Search Message Handlers (Phase 11.1b) ============
1609
+ case "SEARCH_UPDATE": {
1610
+ logger.debug({
1611
+ subscriptionId: message.payload?.subscriptionId,
1612
+ key: message.payload?.key,
1613
+ type: message.payload?.type
1614
+ }, "Received SEARCH_UPDATE");
1615
+ break;
1616
+ }
1597
1617
  }
1598
1618
  if (message.timestamp) {
1599
1619
  this.hlc.update(message.timestamp);
@@ -2357,6 +2377,65 @@ var _SyncEngine = class _SyncEngine {
2357
2377
  }
2358
2378
  }
2359
2379
  }
2380
+ /**
2381
+ * Perform a one-shot BM25 search on the server.
2382
+ *
2383
+ * @param mapName Name of the map to search
2384
+ * @param query Search query text
2385
+ * @param options Search options (limit, minScore, boost)
2386
+ * @returns Promise resolving to search results
2387
+ */
2388
+ async search(mapName, query, options) {
2389
+ if (!this.isAuthenticated()) {
2390
+ throw new Error("Not connected to server");
2391
+ }
2392
+ const requestId = crypto.randomUUID();
2393
+ return new Promise((resolve, reject) => {
2394
+ const timeout = setTimeout(() => {
2395
+ this.pendingSearchRequests.delete(requestId);
2396
+ reject(new Error("Search request timed out"));
2397
+ }, _SyncEngine.SEARCH_TIMEOUT);
2398
+ this.pendingSearchRequests.set(requestId, {
2399
+ resolve: (results) => {
2400
+ clearTimeout(timeout);
2401
+ resolve(results);
2402
+ },
2403
+ reject: (error) => {
2404
+ clearTimeout(timeout);
2405
+ reject(error);
2406
+ },
2407
+ timeout
2408
+ });
2409
+ const sent = this.sendMessage({
2410
+ type: "SEARCH",
2411
+ payload: {
2412
+ requestId,
2413
+ mapName,
2414
+ query,
2415
+ options
2416
+ }
2417
+ });
2418
+ if (!sent) {
2419
+ this.pendingSearchRequests.delete(requestId);
2420
+ clearTimeout(timeout);
2421
+ reject(new Error("Failed to send search request"));
2422
+ }
2423
+ });
2424
+ }
2425
+ /**
2426
+ * Handle search response from server.
2427
+ */
2428
+ handleSearchResponse(payload) {
2429
+ const pending = this.pendingSearchRequests.get(payload.requestId);
2430
+ if (pending) {
2431
+ this.pendingSearchRequests.delete(payload.requestId);
2432
+ if (payload.error) {
2433
+ pending.reject(new Error(payload.error));
2434
+ } else {
2435
+ pending.resolve(payload.results);
2436
+ }
2437
+ }
2438
+ }
2360
2439
  // ============================================
2361
2440
  // Conflict Resolver Client (Phase 5.05)
2362
2441
  // ============================================
@@ -2370,6 +2449,8 @@ var _SyncEngine = class _SyncEngine {
2370
2449
  };
2371
2450
  /** Default timeout for entry processor requests (ms) */
2372
2451
  _SyncEngine.PROCESSOR_TIMEOUT = 3e4;
2452
+ /** Default timeout for search requests (ms) */
2453
+ _SyncEngine.SEARCH_TIMEOUT = 3e4;
2373
2454
  var SyncEngine = _SyncEngine;
2374
2455
 
2375
2456
  // src/TopGunClient.ts
@@ -3035,6 +3116,216 @@ var EventJournalReader = class {
3035
3116
  }
3036
3117
  };
3037
3118
 
3119
+ // src/SearchHandle.ts
3120
+ var SearchHandle = class {
3121
+ constructor(syncEngine, mapName, query, options) {
3122
+ /** Current results map (key → result) */
3123
+ this.results = /* @__PURE__ */ new Map();
3124
+ /** Result change listeners */
3125
+ this.listeners = /* @__PURE__ */ new Set();
3126
+ /** Whether the handle has been disposed */
3127
+ this.disposed = false;
3128
+ this.syncEngine = syncEngine;
3129
+ this.mapName = mapName;
3130
+ this._query = query;
3131
+ this._options = options;
3132
+ this.subscriptionId = crypto.randomUUID();
3133
+ this.messageHandler = this.handleMessage.bind(this);
3134
+ this.syncEngine.on("message", this.messageHandler);
3135
+ this.sendSubscribe();
3136
+ }
3137
+ /**
3138
+ * Handle incoming messages (both SEARCH_RESP and SEARCH_UPDATE).
3139
+ */
3140
+ handleMessage(message) {
3141
+ if (message.type === "SEARCH_RESP") {
3142
+ this.handleSearchResponse(message);
3143
+ } else if (message.type === "SEARCH_UPDATE") {
3144
+ this.handleSearchUpdate(message);
3145
+ }
3146
+ }
3147
+ /**
3148
+ * Get the current query string.
3149
+ */
3150
+ get query() {
3151
+ return this._query;
3152
+ }
3153
+ /**
3154
+ * Subscribe to result changes.
3155
+ * Callback is immediately called with current results.
3156
+ *
3157
+ * @param callback - Function called with updated results
3158
+ * @returns Unsubscribe function
3159
+ */
3160
+ subscribe(callback) {
3161
+ if (this.disposed) {
3162
+ throw new Error("SearchHandle has been disposed");
3163
+ }
3164
+ this.listeners.add(callback);
3165
+ callback(this.getResults());
3166
+ return () => {
3167
+ this.listeners.delete(callback);
3168
+ };
3169
+ }
3170
+ /**
3171
+ * Get current results snapshot sorted by score (highest first).
3172
+ *
3173
+ * @returns Array of search results
3174
+ */
3175
+ getResults() {
3176
+ return Array.from(this.results.values()).sort((a, b) => b.score - a.score);
3177
+ }
3178
+ /**
3179
+ * Get result count.
3180
+ */
3181
+ get size() {
3182
+ return this.results.size;
3183
+ }
3184
+ /**
3185
+ * Update the search query.
3186
+ * Triggers a new subscription with the updated query.
3187
+ *
3188
+ * @param query - New query string
3189
+ */
3190
+ setQuery(query) {
3191
+ if (this.disposed) {
3192
+ throw new Error("SearchHandle has been disposed");
3193
+ }
3194
+ if (query === this._query) {
3195
+ return;
3196
+ }
3197
+ this.sendUnsubscribe();
3198
+ this.results.clear();
3199
+ this._query = query;
3200
+ this.subscriptionId = crypto.randomUUID();
3201
+ this.sendSubscribe();
3202
+ this.notifyListeners();
3203
+ }
3204
+ /**
3205
+ * Update search options.
3206
+ *
3207
+ * @param options - New search options
3208
+ */
3209
+ setOptions(options) {
3210
+ if (this.disposed) {
3211
+ throw new Error("SearchHandle has been disposed");
3212
+ }
3213
+ this.sendUnsubscribe();
3214
+ this.results.clear();
3215
+ this._options = options;
3216
+ this.subscriptionId = crypto.randomUUID();
3217
+ this.sendSubscribe();
3218
+ this.notifyListeners();
3219
+ }
3220
+ /**
3221
+ * Dispose of the handle and cleanup resources.
3222
+ * After disposal, the handle cannot be used.
3223
+ */
3224
+ dispose() {
3225
+ if (this.disposed) {
3226
+ return;
3227
+ }
3228
+ this.disposed = true;
3229
+ this.sendUnsubscribe();
3230
+ this.syncEngine.off("message", this.messageHandler);
3231
+ this.results.clear();
3232
+ this.listeners.clear();
3233
+ }
3234
+ /**
3235
+ * Check if handle is disposed.
3236
+ */
3237
+ isDisposed() {
3238
+ return this.disposed;
3239
+ }
3240
+ /**
3241
+ * Send SEARCH_SUB message to server.
3242
+ */
3243
+ sendSubscribe() {
3244
+ this.syncEngine.send({
3245
+ type: "SEARCH_SUB",
3246
+ payload: {
3247
+ subscriptionId: this.subscriptionId,
3248
+ mapName: this.mapName,
3249
+ query: this._query,
3250
+ options: this._options
3251
+ }
3252
+ });
3253
+ }
3254
+ /**
3255
+ * Send SEARCH_UNSUB message to server.
3256
+ */
3257
+ sendUnsubscribe() {
3258
+ this.syncEngine.send({
3259
+ type: "SEARCH_UNSUB",
3260
+ payload: {
3261
+ subscriptionId: this.subscriptionId
3262
+ }
3263
+ });
3264
+ }
3265
+ /**
3266
+ * Handle SEARCH_RESP message (initial results).
3267
+ */
3268
+ handleSearchResponse(message) {
3269
+ if (message.type !== "SEARCH_RESP") return;
3270
+ if (message.payload?.requestId !== this.subscriptionId) return;
3271
+ const { results } = message.payload;
3272
+ if (Array.isArray(results)) {
3273
+ for (const result of results) {
3274
+ this.results.set(result.key, {
3275
+ key: result.key,
3276
+ value: result.value,
3277
+ score: result.score,
3278
+ matchedTerms: result.matchedTerms || []
3279
+ });
3280
+ }
3281
+ this.notifyListeners();
3282
+ }
3283
+ }
3284
+ /**
3285
+ * Handle SEARCH_UPDATE message (delta updates).
3286
+ */
3287
+ handleSearchUpdate(message) {
3288
+ if (message.type !== "SEARCH_UPDATE") return;
3289
+ if (message.payload?.subscriptionId !== this.subscriptionId) return;
3290
+ const { key, value, score, matchedTerms, type } = message.payload;
3291
+ switch (type) {
3292
+ case "ENTER":
3293
+ this.results.set(key, {
3294
+ key,
3295
+ value,
3296
+ score,
3297
+ matchedTerms: matchedTerms || []
3298
+ });
3299
+ break;
3300
+ case "UPDATE":
3301
+ const existing = this.results.get(key);
3302
+ if (existing) {
3303
+ existing.score = score;
3304
+ existing.matchedTerms = matchedTerms || [];
3305
+ existing.value = value;
3306
+ }
3307
+ break;
3308
+ case "LEAVE":
3309
+ this.results.delete(key);
3310
+ break;
3311
+ }
3312
+ this.notifyListeners();
3313
+ }
3314
+ /**
3315
+ * Notify all listeners of result changes.
3316
+ */
3317
+ notifyListeners() {
3318
+ const results = this.getResults();
3319
+ for (const listener of this.listeners) {
3320
+ try {
3321
+ listener(results);
3322
+ } catch (err) {
3323
+ console.error("SearchHandle listener error:", err);
3324
+ }
3325
+ }
3326
+ }
3327
+ };
3328
+
3038
3329
  // src/cluster/ClusterClient.ts
3039
3330
  import {
3040
3331
  DEFAULT_CONNECTION_POOL_CONFIG as DEFAULT_CONNECTION_POOL_CONFIG2,
@@ -4859,6 +5150,76 @@ var TopGunClient = class {
4859
5150
  return this.syncEngine.onBackpressure(event, listener);
4860
5151
  }
4861
5152
  // ============================================
5153
+ // Full-Text Search API (Phase 11.1a)
5154
+ // ============================================
5155
+ /**
5156
+ * Perform a one-shot BM25 search on the server.
5157
+ *
5158
+ * Searches the specified map using BM25 ranking algorithm.
5159
+ * Requires FTS to be enabled for the map on the server.
5160
+ *
5161
+ * @param mapName Name of the map to search
5162
+ * @param query Search query text
5163
+ * @param options Search options
5164
+ * @returns Promise resolving to search results sorted by relevance
5165
+ *
5166
+ * @example
5167
+ * ```typescript
5168
+ * const results = await client.search<Article>('articles', 'machine learning', {
5169
+ * limit: 20,
5170
+ * minScore: 0.5,
5171
+ * boost: { title: 2.0, body: 1.0 }
5172
+ * });
5173
+ *
5174
+ * for (const result of results) {
5175
+ * console.log(`${result.key}: ${result.value.title} (score: ${result.score})`);
5176
+ * }
5177
+ * ```
5178
+ */
5179
+ async search(mapName, query, options) {
5180
+ return this.syncEngine.search(mapName, query, options);
5181
+ }
5182
+ // ============================================
5183
+ // Live Search API (Phase 11.1b)
5184
+ // ============================================
5185
+ /**
5186
+ * Subscribe to live search results with real-time updates.
5187
+ *
5188
+ * Unlike the one-shot `search()` method, `searchSubscribe()` returns a handle
5189
+ * that receives delta updates (ENTER/UPDATE/LEAVE) when documents change.
5190
+ * This is ideal for live search UIs that need to reflect data changes.
5191
+ *
5192
+ * @param mapName Name of the map to search
5193
+ * @param query Search query text
5194
+ * @param options Search options (limit, minScore, boost)
5195
+ * @returns SearchHandle for managing the subscription
5196
+ *
5197
+ * @example
5198
+ * ```typescript
5199
+ * const handle = client.searchSubscribe<Article>('articles', 'machine learning', {
5200
+ * limit: 20,
5201
+ * minScore: 0.5
5202
+ * });
5203
+ *
5204
+ * // Subscribe to result changes
5205
+ * const unsubscribe = handle.subscribe((results) => {
5206
+ * setSearchResults(results);
5207
+ * });
5208
+ *
5209
+ * // Update query dynamically
5210
+ * handle.setQuery('deep learning');
5211
+ *
5212
+ * // Get current snapshot
5213
+ * const snapshot = handle.getResults();
5214
+ *
5215
+ * // Cleanup when done
5216
+ * handle.dispose();
5217
+ * ```
5218
+ */
5219
+ searchSubscribe(mapName, query, options) {
5220
+ return new SearchHandle(this.syncEngine, mapName, query, options);
5221
+ }
5222
+ // ============================================
4862
5223
  // Entry Processor API (Phase 5.03)
4863
5224
  // ============================================
4864
5225
  /**
@@ -5471,6 +5832,7 @@ export {
5471
5832
  PartitionRouter,
5472
5833
  Predicates,
5473
5834
  QueryHandle,
5835
+ SearchHandle,
5474
5836
  SingleServerProvider,
5475
5837
  SyncEngine,
5476
5838
  SyncState,