@wvdsh/sdk-js 1.3.5 → 1.3.6
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.d.ts +9 -12
- package/dist/index.js +86 -52
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -278,6 +278,8 @@ declare class LobbyManager extends WavedashManager {
|
|
|
278
278
|
private recentMessageIds;
|
|
279
279
|
private maybeBeingDeletedLobbyIds;
|
|
280
280
|
private resetMaybeBeingDeletedLobbyIdTimeouts;
|
|
281
|
+
private static readonly METADATA_UPDATE_THROTTLE_MS;
|
|
282
|
+
private inFlightMetadataUpdate;
|
|
281
283
|
private cachedLobbies;
|
|
282
284
|
private unsubscribeLobbyInvites;
|
|
283
285
|
private seenInviteIds;
|
|
@@ -295,7 +297,6 @@ declare class LobbyManager extends WavedashManager {
|
|
|
295
297
|
getHostId(lobbyId: GenericId<"lobbies">): GenericId<"users"> | null;
|
|
296
298
|
getLobbyData(lobbyId: GenericId<"lobbies">, key: string): string | number | null;
|
|
297
299
|
deleteLobbyData(lobbyId: GenericId<"lobbies">, key: string): boolean;
|
|
298
|
-
private debouncedMetadataUpdate;
|
|
299
300
|
setLobbyData(lobbyId: GenericId<"lobbies">, key: string, value: string | number | null): boolean;
|
|
300
301
|
getLobbyMaxPlayers(lobbyId: GenericId<"lobbies">): number;
|
|
301
302
|
getNumLobbyUsers(lobbyId: GenericId<"lobbies">): number;
|
|
@@ -333,7 +334,8 @@ declare class LobbyManager extends WavedashManager {
|
|
|
333
334
|
* Called during session end to ensure no lingering listeners.
|
|
334
335
|
*/
|
|
335
336
|
destroy(): void;
|
|
336
|
-
private
|
|
337
|
+
private throttledSetMetadata;
|
|
338
|
+
private setMetadata;
|
|
337
339
|
/**
|
|
338
340
|
* Process user updates and emit individual user events
|
|
339
341
|
* @param newUsers - The updated list of lobby users
|
|
@@ -556,10 +558,6 @@ declare class P2PManager extends WavedashManager {
|
|
|
556
558
|
private decodeBinaryMessage;
|
|
557
559
|
}
|
|
558
560
|
|
|
559
|
-
type StatEntry = {
|
|
560
|
-
identifier: string;
|
|
561
|
-
value: number;
|
|
562
|
-
};
|
|
563
561
|
declare class StatsManager extends WavedashManager {
|
|
564
562
|
private stats;
|
|
565
563
|
private unlockedAchievements;
|
|
@@ -569,24 +567,23 @@ declare class StatsManager extends WavedashManager {
|
|
|
569
567
|
private knownAchievementIds;
|
|
570
568
|
private loaded;
|
|
571
569
|
private subscriptions;
|
|
572
|
-
private
|
|
570
|
+
private inFlightPersist;
|
|
571
|
+
private flushRequested;
|
|
573
572
|
constructor(sdk: WavedashSDK);
|
|
574
573
|
destroy(): void;
|
|
575
574
|
private isReady;
|
|
576
575
|
private subscribe;
|
|
577
576
|
requestStats(): Promise<boolean>;
|
|
578
|
-
private
|
|
577
|
+
private throttledPersist;
|
|
579
578
|
storeStats(): boolean;
|
|
579
|
+
private requestPersistFlush;
|
|
580
580
|
private persist;
|
|
581
581
|
getStat(identifier: string): number;
|
|
582
582
|
setStat(identifier: string, value: number, storeNow?: boolean): boolean;
|
|
583
583
|
getAchievement(identifier: string): boolean;
|
|
584
584
|
setAchievement(identifier: string, storeNow?: boolean): boolean;
|
|
585
585
|
/** @destructive - Returns the pending stats and achievements and resets the dirty collections */
|
|
586
|
-
getPendingData
|
|
587
|
-
stats: StatEntry[];
|
|
588
|
-
achievements: string[];
|
|
589
|
-
} | null;
|
|
586
|
+
private getPendingData;
|
|
590
587
|
}
|
|
591
588
|
|
|
592
589
|
/**
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { ConvexClient } from "convex/browser";
|
|
3
3
|
|
|
4
4
|
// src/services/lobby.ts
|
|
5
|
-
import
|
|
5
|
+
import throttle from "lodash.throttle";
|
|
6
6
|
|
|
7
7
|
// src/constants.ts
|
|
8
8
|
import {
|
|
@@ -94,7 +94,7 @@ var WavedashManager = class {
|
|
|
94
94
|
};
|
|
95
95
|
|
|
96
96
|
// src/services/lobby.ts
|
|
97
|
-
var
|
|
97
|
+
var _LobbyManager = class _LobbyManager extends WavedashManager {
|
|
98
98
|
constructor(sdk) {
|
|
99
99
|
super(sdk);
|
|
100
100
|
// Track current lobby state
|
|
@@ -109,6 +109,7 @@ var LobbyManager = class extends WavedashManager {
|
|
|
109
109
|
this.recentMessageIds = [];
|
|
110
110
|
this.maybeBeingDeletedLobbyIds = /* @__PURE__ */ new Set();
|
|
111
111
|
this.resetMaybeBeingDeletedLobbyIdTimeouts = /* @__PURE__ */ new Map();
|
|
112
|
+
this.inFlightMetadataUpdate = null;
|
|
112
113
|
// Cache results of queries for a list of lobbies
|
|
113
114
|
// We'll cache metadata and num users for each lobby and return that info synchronously when requested by the game
|
|
114
115
|
this.cachedLobbies = {};
|
|
@@ -117,9 +118,13 @@ var LobbyManager = class extends WavedashManager {
|
|
|
117
118
|
this.seenInviteIds = /* @__PURE__ */ new Set();
|
|
118
119
|
// Queue for serializing P2P connection updates to prevent race conditions
|
|
119
120
|
this.p2pUpdateQueue = Promise.resolve();
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
// leading: false so the first call doesn't fire synchronously inside setLobbyData
|
|
122
|
+
// (which is called from a tight loop); trailing: true to flush coalesced updates
|
|
123
|
+
// at the end of the window.
|
|
124
|
+
this.throttledSetMetadata = throttle(
|
|
125
|
+
() => this.setMetadata(),
|
|
126
|
+
_LobbyManager.METADATA_UPDATE_THROTTLE_MS,
|
|
127
|
+
{ leading: false, trailing: true }
|
|
123
128
|
);
|
|
124
129
|
/**
|
|
125
130
|
* Process user updates and emit individual user events
|
|
@@ -266,7 +271,7 @@ var LobbyManager = class extends WavedashManager {
|
|
|
266
271
|
this.lobbyMetadata[key] = value;
|
|
267
272
|
}
|
|
268
273
|
this.pendingMetadataUpdates[key] = value;
|
|
269
|
-
this.
|
|
274
|
+
this.throttledSetMetadata();
|
|
270
275
|
return true;
|
|
271
276
|
}
|
|
272
277
|
getLobbyMaxPlayers(lobbyId) {
|
|
@@ -443,7 +448,7 @@ var LobbyManager = class extends WavedashManager {
|
|
|
443
448
|
cleanupLobbyState() {
|
|
444
449
|
const currentLobbyId = this.lobbyId;
|
|
445
450
|
this.lobbyId = null;
|
|
446
|
-
this.
|
|
451
|
+
this.throttledSetMetadata.cancel();
|
|
447
452
|
this.pendingMetadataUpdates = {};
|
|
448
453
|
if (this.unsubscribeLobbyMessages) {
|
|
449
454
|
this.unsubscribeLobbyMessages();
|
|
@@ -501,14 +506,22 @@ var LobbyManager = class extends WavedashManager {
|
|
|
501
506
|
this.seenInviteIds.clear();
|
|
502
507
|
this.cachedLobbies = {};
|
|
503
508
|
}
|
|
504
|
-
|
|
509
|
+
setMetadata() {
|
|
510
|
+
if (this.inFlightMetadataUpdate !== null) return;
|
|
511
|
+
if (this.lobbyId === null) return;
|
|
512
|
+
if (Object.keys(this.pendingMetadataUpdates).length === 0) return;
|
|
505
513
|
const updates = this.pendingMetadataUpdates;
|
|
506
514
|
this.pendingMetadataUpdates = {};
|
|
507
|
-
this.sdk.convexClient.mutation(api.sdk.gameLobby.setLobbyMetadata, {
|
|
515
|
+
this.inFlightMetadataUpdate = this.sdk.convexClient.mutation(api.sdk.gameLobby.setLobbyMetadata, {
|
|
508
516
|
lobbyId: this.lobbyId,
|
|
509
517
|
updates
|
|
510
518
|
}).catch((error) => {
|
|
511
519
|
this.sdk.logger.error("Error updating lobby metadata:", error);
|
|
520
|
+
}).finally(() => {
|
|
521
|
+
this.inFlightMetadataUpdate = null;
|
|
522
|
+
if (Object.keys(this.pendingMetadataUpdates).length > 0) {
|
|
523
|
+
this.throttledSetMetadata();
|
|
524
|
+
}
|
|
512
525
|
});
|
|
513
526
|
}
|
|
514
527
|
// ================
|
|
@@ -547,6 +560,10 @@ var LobbyManager = class extends WavedashManager {
|
|
|
547
560
|
}
|
|
548
561
|
}
|
|
549
562
|
};
|
|
563
|
+
// Throttle (not debounce) batches rapid setLobbyData calls; the in-flight
|
|
564
|
+
// gate in setMetadata prevents OCC self-conflicts.
|
|
565
|
+
_LobbyManager.METADATA_UPDATE_THROTTLE_MS = 150;
|
|
566
|
+
var LobbyManager = _LobbyManager;
|
|
550
567
|
|
|
551
568
|
// src/utils/indexedDB.ts
|
|
552
569
|
var LOCAL_STORAGE_DB_NAME = "/userfs";
|
|
@@ -2424,9 +2441,8 @@ var P2PManager = _P2PManager;
|
|
|
2424
2441
|
|
|
2425
2442
|
// src/services/stats.ts
|
|
2426
2443
|
import { api as api6 } from "@wvdsh/api";
|
|
2427
|
-
import
|
|
2428
|
-
var
|
|
2429
|
-
var PERIODIC_PERSIST_MS = 1e4;
|
|
2444
|
+
import throttle2 from "lodash.throttle";
|
|
2445
|
+
var STORE_THROTTLE_MS = 1e3;
|
|
2430
2446
|
var StatsManager = class extends WavedashManager {
|
|
2431
2447
|
constructor(sdk) {
|
|
2432
2448
|
super(sdk);
|
|
@@ -2444,32 +2460,33 @@ var StatsManager = class extends WavedashManager {
|
|
|
2444
2460
|
this.loaded = { stats: false, achievements: false };
|
|
2445
2461
|
// Subscription cleanup
|
|
2446
2462
|
this.subscriptions = [];
|
|
2447
|
-
//
|
|
2448
|
-
this.
|
|
2463
|
+
// Single in-flight persist mutation; prevents OCC self-conflicts.
|
|
2464
|
+
this.inFlightPersist = null;
|
|
2465
|
+
// Set when a storeNow flush hits the in-flight gate; persist's .finally()
|
|
2466
|
+
// checks this and fires immediately on the next cycle instead of waiting
|
|
2467
|
+
// out the throttle window. (lodash treats the gated flush as a successful
|
|
2468
|
+
// invocation, so without this flag a contended storeNow waits ~THROTTLE_MS.)
|
|
2469
|
+
this.flushRequested = false;
|
|
2449
2470
|
// ================
|
|
2450
2471
|
// Store / Persist
|
|
2451
2472
|
// ================
|
|
2452
|
-
//
|
|
2453
|
-
//
|
|
2454
|
-
//
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2473
|
+
// leading: false so a single setStat doesn't fire synchronously inside the
|
|
2474
|
+
// setter; trailing: true to flush coalesced edits at the end of the window.
|
|
2475
|
+
// storeNow=true (and storeStats()) call .flush() to fire the pending invocation
|
|
2476
|
+
// immediately. The in-flight gate in persist() covers mutations that outlast
|
|
2477
|
+
// the throttle window, which would otherwise overlap and cause OCC conflicts.
|
|
2478
|
+
this.throttledPersist = throttle2(
|
|
2479
|
+
() => this.persist(),
|
|
2480
|
+
STORE_THROTTLE_MS,
|
|
2481
|
+
{ leading: false, trailing: true }
|
|
2482
|
+
);
|
|
2459
2483
|
this.subscribe();
|
|
2460
2484
|
this.requestStats().catch((error) => {
|
|
2461
2485
|
this.sdk.logger.error("Initial stats fetch failed:", error);
|
|
2462
2486
|
});
|
|
2463
|
-
this.periodicPersistInterval = setInterval(() => {
|
|
2464
|
-
void this.persist();
|
|
2465
|
-
}, PERIODIC_PERSIST_MS);
|
|
2466
2487
|
}
|
|
2467
2488
|
destroy() {
|
|
2468
|
-
this.
|
|
2469
|
-
if (this.periodicPersistInterval !== null) {
|
|
2470
|
-
clearInterval(this.periodicPersistInterval);
|
|
2471
|
-
this.periodicPersistInterval = null;
|
|
2472
|
-
}
|
|
2489
|
+
this.throttledPersist.cancel();
|
|
2473
2490
|
for (const unsub of this.subscriptions) unsub();
|
|
2474
2491
|
this.subscriptions = [];
|
|
2475
2492
|
}
|
|
@@ -2534,35 +2551,51 @@ var StatsManager = class extends WavedashManager {
|
|
|
2534
2551
|
}
|
|
2535
2552
|
storeStats() {
|
|
2536
2553
|
if (!this.isReady()) return false;
|
|
2537
|
-
this.
|
|
2538
|
-
this.
|
|
2554
|
+
this.throttledPersist();
|
|
2555
|
+
this.requestPersistFlush();
|
|
2539
2556
|
return true;
|
|
2540
2557
|
}
|
|
2541
|
-
|
|
2558
|
+
// Force-fire the throttled persist now. If a mutation is already in flight,
|
|
2559
|
+
// the gate will swallow the flush(), so we also flag flushRequested so the
|
|
2560
|
+
// next .finally() flushes again instead of waiting a full throttle window.
|
|
2561
|
+
requestPersistFlush() {
|
|
2562
|
+
if (this.inFlightPersist !== null) this.flushRequested = true;
|
|
2563
|
+
this.throttledPersist.flush();
|
|
2564
|
+
}
|
|
2565
|
+
persist() {
|
|
2566
|
+
if (this.inFlightPersist !== null) return;
|
|
2567
|
+
if (this.dirtyStats.size === 0 && this.dirtyAchievements.size === 0) return;
|
|
2542
2568
|
const pending = this.getPendingData();
|
|
2543
2569
|
if (!pending) return;
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
]);
|
|
2570
|
+
this.inFlightPersist = Promise.all([
|
|
2571
|
+
pending.stats.length > 0 ? this.sdk.convexClient.mutation(
|
|
2572
|
+
api6.sdk.gameAchievements.setUserGameStats,
|
|
2573
|
+
{ stats: pending.stats }
|
|
2574
|
+
) : Promise.resolve(),
|
|
2575
|
+
pending.achievements.length > 0 ? this.sdk.convexClient.mutation(
|
|
2576
|
+
api6.sdk.gameAchievements.setUserGameAchievements,
|
|
2577
|
+
{ achievements: pending.achievements }
|
|
2578
|
+
) : Promise.resolve()
|
|
2579
|
+
]).then(() => {
|
|
2555
2580
|
this.sdk.gameEventManager.notifyGame(WavedashEvents.STATS_STORED, {
|
|
2556
2581
|
success: true
|
|
2557
2582
|
});
|
|
2558
|
-
}
|
|
2583
|
+
}).catch((error) => {
|
|
2559
2584
|
const message = error instanceof Error ? error.message : `Error storing stats: ${error}`;
|
|
2560
2585
|
this.sdk.logger.error(message);
|
|
2561
2586
|
this.sdk.gameEventManager.notifyGame(WavedashEvents.STATS_STORED, {
|
|
2562
2587
|
success: false,
|
|
2563
2588
|
message
|
|
2564
2589
|
});
|
|
2565
|
-
}
|
|
2590
|
+
}).finally(() => {
|
|
2591
|
+
this.inFlightPersist = null;
|
|
2592
|
+
const shouldFlushNow = this.flushRequested;
|
|
2593
|
+
this.flushRequested = false;
|
|
2594
|
+
if (this.dirtyStats.size > 0 || this.dirtyAchievements.size > 0) {
|
|
2595
|
+
this.throttledPersist();
|
|
2596
|
+
if (shouldFlushNow) this.throttledPersist.flush();
|
|
2597
|
+
}
|
|
2598
|
+
});
|
|
2566
2599
|
}
|
|
2567
2600
|
// ================
|
|
2568
2601
|
// Stats
|
|
@@ -2576,8 +2609,9 @@ var StatsManager = class extends WavedashManager {
|
|
|
2576
2609
|
if (this.stats.get(identifier) !== value) {
|
|
2577
2610
|
this.stats.set(identifier, value);
|
|
2578
2611
|
this.dirtyStats.add(identifier);
|
|
2612
|
+
this.throttledPersist();
|
|
2579
2613
|
}
|
|
2580
|
-
if (storeNow) this.
|
|
2614
|
+
if (storeNow) this.requestPersistFlush();
|
|
2581
2615
|
return true;
|
|
2582
2616
|
}
|
|
2583
2617
|
// ================
|
|
@@ -2594,13 +2628,11 @@ var StatsManager = class extends WavedashManager {
|
|
|
2594
2628
|
if (!this.unlockedAchievements.has(identifier)) {
|
|
2595
2629
|
this.unlockedAchievements.add(identifier);
|
|
2596
2630
|
this.dirtyAchievements.add(identifier);
|
|
2631
|
+
this.throttledPersist();
|
|
2597
2632
|
}
|
|
2598
|
-
if (storeNow) this.
|
|
2633
|
+
if (storeNow) this.requestPersistFlush();
|
|
2599
2634
|
return true;
|
|
2600
2635
|
}
|
|
2601
|
-
// ================
|
|
2602
|
-
// Session End
|
|
2603
|
-
// ================
|
|
2604
2636
|
/** @destructive - Returns the pending stats and achievements and resets the dirty collections */
|
|
2605
2637
|
getPendingData() {
|
|
2606
2638
|
if (this.dirtyStats.size === 0 && this.dirtyAchievements.size === 0) {
|
|
@@ -2705,8 +2737,10 @@ var HeartbeatManager = class extends WavedashManager {
|
|
|
2705
2737
|
if (!reestablish && this.heartbeatInFlight) return;
|
|
2706
2738
|
this.heartbeatInFlight = true;
|
|
2707
2739
|
this.sdk.convexClient.mutation(api7.sdk.presence.heartbeat, {
|
|
2708
|
-
...reestablish ? {
|
|
2709
|
-
|
|
2740
|
+
...reestablish ? {
|
|
2741
|
+
data: { forceUpdate: true },
|
|
2742
|
+
deviceFingerprint: this.deviceFingerprint
|
|
2743
|
+
} : {}
|
|
2710
2744
|
}).then((accepted) => {
|
|
2711
2745
|
if (accepted) {
|
|
2712
2746
|
this.lastHeartbeatTime = Date.now();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wvdsh/sdk-js",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Wavedash JavaScript SDK",
|
|
6
6
|
"main": "./dist/client.js",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@eslint/js": "^9.39.2",
|
|
43
|
-
"@types/lodash.
|
|
43
|
+
"@types/lodash.throttle": "^4.1.9",
|
|
44
44
|
"eslint": "^9.39.2",
|
|
45
45
|
"eslint-config-prettier": "^10.1.8",
|
|
46
46
|
"prettier": "^3.7.4",
|
|
@@ -51,6 +51,6 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@wvdsh/api": "^0.1.14",
|
|
53
53
|
"convex": "^1.34.0",
|
|
54
|
-
"lodash.
|
|
54
|
+
"lodash.throttle": "^4.1.1"
|
|
55
55
|
}
|
|
56
56
|
}
|