@wardx/core 0.5.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/README.md CHANGED
@@ -79,11 +79,16 @@ const settings = {
79
79
 
80
80
  const core = new WardxCore(settings);
81
81
 
82
- core.counter('match.completed', { mode: 'ranked' }).inc();
83
- core.counter('coins.awarded').add(25);
84
- core.gauge('players.online').set(12921);
85
- core.histogram('request.duration').observe(42);
86
- core.distinct('shot.traffic.hids', { result: 'violating' }).add(hid);
82
+ const completed = core.counter('match.completed', { mode: 'ranked' });
83
+ completed.inc();
84
+ const coinsAwarded = core.counter('coins.awarded');
85
+ coinsAwarded.add(25);
86
+ const playersOnline = core.gauge('players.online');
87
+ playersOnline.set(12921);
88
+ const requestDuration = core.histogram('request.duration');
89
+ requestDuration.observe(42);
90
+ const shotTrafficHids = core.distinct('shot.traffic.hids', { result: 'violating' });
91
+ shotTrafficHids.add(hid);
87
92
 
88
93
  const endTimer = core.timer('matchmaking.duration');
89
94
  endTimer({ result: 'success' });
@@ -108,8 +113,10 @@ const frames = core.takePendingFrames();
108
113
  `histogram(name).observe(value)` records a finite value into buckets. You can set buckets:
109
114
 
110
115
  ```js
111
- core.histogram('request.duration', { buckets: [10, 25, 50, 100] }).observe(42);
112
- core.histogram('coins.award_size').observe(80, { grantId: 'g-80' });
116
+ const requestDuration = core.histogram('request.duration', { buckets: [10, 25, 50, 100] });
117
+ requestDuration.observe(42);
118
+ const coinsAwardSize = core.histogram('coins.award_size');
119
+ coinsAwardSize.observe(80, { grantId: 'g-80' });
113
120
  ```
114
121
 
115
122
  `observe(value, attrs)` keeps `attrs` only when `value` is the window max. The frame stores that pair as `exemplar`. Attrs use the same key and value limits as dimensions. Do not change the buckets of an existing series. The engine throws an error.
@@ -117,7 +124,7 @@ core.histogram('coins.award_size').observe(80, { grantId: 'g-80' });
117
124
  `timer(name, dims)` starts a timer. The returned function records the duration in milliseconds into a histogram. You can add dimensions when you stop the timer.
118
125
 
119
126
  `distinct(name, dims).add(identifier)` updates a fixed HyperLogLog sketch. The
120
- engine hashes the identifier as `SHA-256(privacySalt || 0x00 || identifier)` and
127
+ engine hashes the identifier as `XXHash64(privacySalt || 0x00 || identifier)` and
121
128
  discards it immediately; the frame contains only 512 HLL registers (`p=9`,
122
129
  about 4.6% standard error). Keep `privacySalt` stable across workers and time
123
130
  windows so equal identifiers map to equal registers.
@@ -239,7 +246,7 @@ bucket = hash / 2^32
239
246
 
240
247
  If `bucket >= allocation`, `assignVariant` returns `null`.
241
248
 
242
- `subjectHash` returns 64 lowercase hex digits of `SHA-256(UTF8(privacySalt) || 0x00 || UTF8(subjectId))`.
249
+ `subjectHash` returns 16 lowercase hex digits of `XXHash64(UTF8(privacySalt) || 0x00 || UTF8(subjectId))`.
243
250
 
244
251
  ## Use case 5: Build a frame for a custom transport
245
252
 
@@ -250,7 +257,8 @@ If `bucket >= allocation`, `assignVariant` returns `null`.
250
257
  ```js
251
258
  import { FrameBuilder, PROTOCOL_VERSION, SDK_NAME, PLATFORM } from '@wardx/core';
252
259
 
253
- core.counter('match.completed').inc();
260
+ const matchCompleted = core.counter('match.completed');
261
+ matchCompleted.inc();
254
262
  const batch = core.snapshotIfDirty();
255
263
  if (batch) {
256
264
  const frames = core.takePendingFrames();
@@ -311,3 +319,13 @@ server persists UTC cohorts and exact received-user D1/D7/D30 returns; the core
311
319
  does not infer activity or keep a durable client identity/outbox. Use the same
312
320
  user ID and privacy salt across sessions and clients. See the
313
321
  [Node retention contract](../node/README.md#user-retention-d1--d7--d30).
322
+
323
+ ## Reuse metric handles
324
+
325
+ Bind counters, gauges, histograms, and distinct handles once per stable name and
326
+ dimension set. Keep them in the owning module or component and measure through
327
+ the handles in callbacks. This avoids repeated dimension validation and registry
328
+ lookup. Normal snapshots reset values while preserving handles; recreate bindings
329
+ when replacing the core. Timer stop functions belong to individual operations.
330
+ See the [Node pattern](../node/README.md#recommended-bind-once-measure-through-handles)
331
+ and [C# field pattern](../../clients/csharp/README.md#recommended-keep-handles-as-fields).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wardx/core",
3
- "version": "0.5.0",
3
+ "version": "0.8.0",
4
4
  "description": "Runtime-agnostic Wardx engine for metrics, events, logs, Remote Config, and experiments.",
5
5
  "keywords": [
6
6
  "wardx",
@@ -40,5 +40,8 @@
40
40
  ],
41
41
  "publishConfig": {
42
42
  "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "xxhash-wasm": "1.1.0"
43
46
  }
44
47
  }
@@ -1,4 +1,6 @@
1
- import { createHash } from 'node:crypto';
1
+ import xxhash from 'xxhash-wasm';
2
+
3
+ const { h64 } = await xxhash();
2
4
 
3
5
  const encoder = new TextEncoder();
4
6
 
@@ -24,10 +26,10 @@ export function assignmentHash(experimentId, subjectId, salt) {
24
26
  return fnv1a32(`${experimentId}:${subjectId}:${salt}`);
25
27
  }
26
28
 
29
+ export function subjectHash64(projectSalt, subjectId) {
30
+ return h64(`${projectSalt}\0${subjectId}`);
31
+ }
32
+
27
33
  export function subjectHash(projectSalt, subjectId) {
28
- return createHash('sha256')
29
- .update(projectSalt, 'utf8')
30
- .update('\0', 'utf8')
31
- .update(subjectId, 'utf8')
32
- .digest('hex');
34
+ return subjectHash64(projectSalt, subjectId).toString(16).padStart(16, '0');
33
35
  }
package/src/index.d.ts CHANGED
@@ -67,6 +67,7 @@ export interface SdkDefaults {
67
67
  }
68
68
 
69
69
  export interface CreateWardxOptions extends Partial<SdkDefaults> {
70
+ enabled?: boolean;
70
71
  endpoint: string;
71
72
  projectKey: string;
72
73
  project: string;
@@ -89,6 +90,7 @@ export interface CoreSettings extends SdkDefaults {
89
90
  }
90
91
 
91
92
  export interface ResolvedSettings extends SdkDefaults {
93
+ enabled?: boolean;
92
94
  endpoint: string;
93
95
  projectKey: string;
94
96
  project: string;
@@ -1,8 +1,9 @@
1
- import { createHash } from 'node:crypto';
1
+ import { subjectHash64 } from '../config/hash.js';
2
2
 
3
3
  export const HLL_PRECISION = 9;
4
4
  export const HLL_REGISTER_COUNT = 1 << HLL_PRECISION;
5
5
  export const HLL_MAX_RANK = 64 - HLL_PRECISION + 1;
6
+ const INDEX_SHIFT = BigInt(64 - HLL_PRECISION);
6
7
 
7
8
  function assertRegisters(registers) {
8
9
  if (!(registers instanceof Uint8Array) || registers.length !== HLL_REGISTER_COUNT) {
@@ -13,13 +14,10 @@ function assertRegisters(registers) {
13
14
  }
14
15
  }
15
16
 
16
- function rankAfterIndex(digest) {
17
- let rank = 1;
18
- for (let bit = HLL_PRECISION; bit < 64; bit++) {
19
- if ((digest[bit >> 3] & (1 << (7 - (bit & 7)))) !== 0) return rank;
20
- rank += 1;
21
- }
22
- return rank;
17
+ export function rankAfterIndex(digest) {
18
+ const remainingHigh = Number(digest >> 32n) << HLL_PRECISION;
19
+ if (remainingHigh !== 0) return Math.clz32(remainingHigh) + 1;
20
+ return 32 - HLL_PRECISION + Math.clz32(Number(digest & 0xffffffffn)) + 1;
23
21
  }
24
22
 
25
23
  export function encodeHllRegisters(registers) {
@@ -82,12 +80,8 @@ export class HyperLogLog {
82
80
  if (typeof identifier !== 'string' || identifier.length === 0) {
83
81
  throw new Error('distinct.add requires a non-empty string');
84
82
  }
85
- const digest = createHash('sha256')
86
- .update(this.privacySalt, 'utf8')
87
- .update('\0', 'utf8')
88
- .update(identifier, 'utf8')
89
- .digest();
90
- const index = (digest[0] << 1) | (digest[1] >> 7);
83
+ const digest = subjectHash64(this.privacySalt, identifier);
84
+ const index = Number(digest >> INDEX_SHIFT);
91
85
  const rank = rankAfterIndex(digest);
92
86
  if (rank > this.registers[index]) this.registers[index] = rank;
93
87
  this.dirty = true;