@dxos/keys 0.8.4-main.fd6878d → 0.8.4-main.fffef41

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/keys",
3
- "version": "0.8.4-main.fd6878d",
3
+ "version": "0.8.4-main.fffef41",
4
4
  "description": "Key utils and definitions.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -25,11 +25,11 @@
25
25
  "src"
26
26
  ],
27
27
  "dependencies": {
28
- "effect": "3.17.7",
28
+ "effect": "3.18.3",
29
29
  "ulidx": "^2.3.0",
30
- "@dxos/debug": "0.8.4-main.fd6878d",
31
- "@dxos/node-std": "0.8.4-main.fd6878d",
32
- "@dxos/invariant": "0.8.4-main.fd6878d"
30
+ "@dxos/debug": "0.8.4-main.fffef41",
31
+ "@dxos/invariant": "0.8.4-main.fffef41",
32
+ "@dxos/node-std": "0.8.4-main.fffef41"
33
33
  },
34
34
  "devDependencies": {
35
35
  "base32-decode": "^1.0.0",
package/src/dxn.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import type { InspectOptionsStylized, inspect } from 'node:util';
6
6
 
7
- import { Schema } from 'effect';
7
+ import * as Schema from 'effect/Schema';
8
8
 
9
9
  import { type DevtoolsFormatter, devtoolsFormatter, inspectCustom } from '@dxos/debug';
10
10
  import { assertArgument, invariant } from '@dxos/invariant';
@@ -29,6 +29,12 @@ export const QueueSubspaceTags = Object.freeze({
29
29
 
30
30
  export type QueueSubspaceTag = (typeof QueueSubspaceTags)[keyof typeof QueueSubspaceTags];
31
31
 
32
+ // TODO(burdon): Refactor.
33
+ // Consider: https://github.com/multiformats/multiaddr
34
+ // dxn:echo:[<space-id>:[<queue-id>:]]<object-id>
35
+ // dxn:echo:[S/<space-id>:[Q/<queue-id>:]]<object-id>
36
+ // dxn:type:dxos.org/markdown/Contact
37
+
32
38
  /**
33
39
  * DXN unambiguously names a resource like an ECHO object, schema definition, plugin, etc.
34
40
  * Each DXN starts with a dxn prefix, followed by a resource kind.
@@ -68,16 +74,16 @@ export class DXN {
68
74
  */
69
75
  static kind = Object.freeze({
70
76
  /**
71
- * dxn:type:<type name>[:<version>]
77
+ * dxn:type:<type_name>[:<version>]
72
78
  */
73
79
  TYPE: 'type',
74
80
 
75
81
  /**
76
- * dxn:echo:<space id>:<echo id>
77
- * dxn:echo:@:<echo id>
82
+ * dxn:echo:<space_id>:<echo_id>
83
+ * dxn:echo:@:<echo_id>
78
84
  */
79
- // TODO(burdon): Rename to OBJECT? (BREAKING CHANGE).
80
- // TODO(burdon): Add separate Kind for space.
85
+ // TODO(burdon): Rename to OBJECT? (BREAKING CHANGE to update "echo").
86
+ // TODO(burdon): Add separate Kind for space?
81
87
  ECHO: 'echo',
82
88
 
83
89
  /**
@@ -89,14 +95,19 @@ export class DXN {
89
95
  QUEUE: 'queue',
90
96
  });
91
97
 
92
- get kind() {
93
- return this.#kind;
94
- }
95
-
98
+ /**
99
+ * Exactly equals.
100
+ */
96
101
  static equals(a: DXN, b: DXN): boolean {
97
102
  return a.kind === b.kind && a.parts.length === b.parts.length && a.parts.every((part, i) => part === b.parts[i]);
98
103
  }
99
104
 
105
+ static equalsEchoId(a: DXN, b: DXN): boolean {
106
+ const a1 = a.asEchoDXN();
107
+ const b1 = b.asEchoDXN();
108
+ return !!a1 && !!b1 && a1.echoId === b1.echoId;
109
+ }
110
+
100
111
  // TODO(burdon): Rename isValid.
101
112
  static isDXNString(dxn: string): boolean {
102
113
  return dxn.startsWith('dxn:');
@@ -129,14 +140,14 @@ export class DXN {
129
140
  }
130
141
 
131
142
  /**
132
- * @example `dxn:type:example.com/type/Contact`
143
+ * @example `dxn:type:example.com/type/Person`
133
144
  */
134
145
  static fromTypename(typename: string): DXN {
135
146
  return new DXN(DXN.kind.TYPE, [typename]);
136
147
  }
137
148
 
138
149
  /**
139
- * @example `dxn:type:example.com/type/Contact:0.1.0`
150
+ * @example `dxn:type:example.com/type/Person:0.1.0`
140
151
  */
141
152
  // TODO(dmaretskyi): Consider using @ as the version separator.
142
153
  static fromTypenameAndVersion(typename: string, version: string): DXN {
@@ -148,7 +159,7 @@ export class DXN {
148
159
  */
149
160
  static fromSpaceAndObjectId(spaceId: SpaceId, objectId: ObjectId): DXN {
150
161
  assertArgument(SpaceId.isValid(spaceId), `Invalid space ID: ${spaceId}`);
151
- assertArgument(ObjectId.isValid(objectId), `Invalid object ID: ${objectId}`);
162
+ assertArgument(ObjectId.isValid(objectId), 'objectId', `Invalid object ID: ${objectId}`);
152
163
  return new DXN(DXN.kind.ECHO, [spaceId, objectId]);
153
164
  }
154
165
 
@@ -156,14 +167,14 @@ export class DXN {
156
167
  * @example `dxn:echo:@:01J00J9B45YHYSGZQTQMSKMGJ6`
157
168
  */
158
169
  static fromLocalObjectId(id: string): DXN {
159
- assertArgument(ObjectId.isValid(id), `Invalid object ID: ${id}`);
170
+ assertArgument(ObjectId.isValid(id), 'id', `Invalid object ID: ${id}`);
160
171
  return new DXN(DXN.kind.ECHO, [LOCAL_SPACE_TAG, id]);
161
172
  }
162
173
 
163
174
  static fromQueue(subspaceTag: QueueSubspaceTag, spaceId: SpaceId, queueId: ObjectId, objectId?: ObjectId) {
164
175
  assertArgument(SpaceId.isValid(spaceId), `Invalid space ID: ${spaceId}`);
165
- assertArgument(ObjectId.isValid(queueId), `Invalid queue ID: ${queueId}`);
166
- assertArgument(!objectId || ObjectId.isValid(objectId), `Invalid object ID: ${objectId}`);
176
+ assertArgument(ObjectId.isValid(queueId), 'queueId', `Invalid queue ID: ${queueId}`);
177
+ assertArgument(!objectId || ObjectId.isValid(objectId), 'objectId', `Invalid object ID: ${objectId}`);
167
178
 
168
179
  return new DXN(DXN.kind.QUEUE, [subspaceTag, spaceId, queueId, ...(objectId ? [objectId] : [])]);
169
180
  }
@@ -172,9 +183,10 @@ export class DXN {
172
183
  #parts: string[];
173
184
 
174
185
  constructor(kind: string, parts: string[]) {
175
- assertArgument(parts.length > 0, `Invalid DXN: ${parts}`);
186
+ assertArgument(parts.length > 0, 'parts', `Invalid DXN: ${parts}`);
176
187
  assertArgument(
177
188
  parts.every((part) => typeof part === 'string' && part.length > 0 && part.indexOf(':') === -1),
189
+ 'parts',
178
190
  `Invalid DXN: ${parts}`,
179
191
  );
180
192
 
@@ -182,12 +194,12 @@ export class DXN {
182
194
  switch (kind) {
183
195
  case DXN.kind.TYPE:
184
196
  if (parts.length > 2) {
185
- throw new Error('Invalid "type" DXN');
197
+ throw new Error('Invalid DXN.kind.TYPE');
186
198
  }
187
199
  break;
188
200
  case DXN.kind.ECHO:
189
201
  if (parts.length !== 2) {
190
- throw new Error('Invalid "echo" DXN');
202
+ throw new Error('Invalid DXN.kind.ECHO');
191
203
  }
192
204
  break;
193
205
  }
@@ -225,6 +237,10 @@ export class DXN {
225
237
  };
226
238
  }
227
239
 
240
+ get kind() {
241
+ return this.#kind;
242
+ }
243
+
228
244
  get parts() {
229
245
  return this.#parts;
230
246
  }
@@ -235,6 +251,10 @@ export class DXN {
235
251
  return this.#parts[0];
236
252
  }
237
253
 
254
+ equals(other: DXN): boolean {
255
+ return DXN.equals(this, other);
256
+ }
257
+
238
258
  hasTypenameOf(typename: string): boolean {
239
259
  return this.#kind === DXN.kind.TYPE && this.#parts.length === 1 && this.#parts[0] === typename;
240
260
  }
@@ -264,6 +284,7 @@ export class DXN {
264
284
  const [spaceId, echoId] = this.#parts;
265
285
  return {
266
286
  spaceId: spaceId === LOCAL_SPACE_TAG ? undefined : (spaceId as SpaceId | undefined),
287
+ // TODO(burdon): objectId.
267
288
  echoId,
268
289
  };
269
290
  }
@@ -294,33 +315,18 @@ export class DXN {
294
315
  }
295
316
  }
296
317
 
297
- // TODO(dmaretskyi): Fluent API:
298
- /*
299
- class DXN {
300
- ...
301
- isEchoDXN(): this is EchoDXN {
302
- return this.#kind === DXN.kind.ECHO;
303
- }
304
- ...
305
- }
306
-
307
- interface EchoDXN extends DXN {
308
- objectId: ObjectId;
309
- }
310
-
311
- declare const dxn: DXN;
312
-
313
- dxn.objectId
314
-
315
- if(dxn.isEchoDXN()) {
316
- dxn.objectId
317
- }
318
- ```
319
-
320
318
  /**
321
319
  * API namespace.
322
320
  */
323
321
  export declare namespace DXN {
322
+ /**
323
+ * DXN represented as a javascript string.
324
+ */
325
+ // TODO(burdon): Use Effect branded string?
326
+ // export const String = S.String.pipe(S.brand('DXN'));
327
+ // export type String = S.To(typoeof String);
328
+ export type String = string & { __DXNString: never };
329
+
324
330
  export type TypeDXN = {
325
331
  type: string;
326
332
  version?: string;
@@ -337,12 +343,4 @@ export declare namespace DXN {
337
343
  queueId: string; // TODO(dmaretskyi): ObjectId.
338
344
  objectId?: string; // TODO(dmaretskyi): ObjectId.
339
345
  };
340
-
341
- /**
342
- * DXN represented as a javascript string.
343
- */
344
- export type String = string & { __DXNString: never };
345
- // TODO(burdon): Make brand.
346
- // export const String = S.String.pipe(S.brand('DXN'));
347
- // export type String = S.To(typoeof String);
348
346
  }
package/src/object-id.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
6
- import { ulid } from 'ulidx';
5
+ import * as Schema from 'effect/Schema';
6
+ import { type PRNG, type ULIDFactory, monotonicFactory } from 'ulidx';
7
7
 
8
8
  // TODO(dmaretskyi): Make brand.
9
9
  // export const ObjectIdBrand: unique symbol = Symbol('@dxos/echo/ObjectId');
@@ -16,9 +16,39 @@ const ObjectIdSchema = Schema.String.pipe(Schema.pattern(/^[0-7][0-9A-HJKMNP-TV-
16
16
  export type ObjectId = typeof ObjectIdSchema.Type;
17
17
 
18
18
  export interface ObjectIdClass extends Schema.SchemaClass<ObjectId, string> {
19
+ /**
20
+ * @returns true if the string is a valid ObjectId.
21
+ */
19
22
  isValid(id: string): id is ObjectId;
23
+
24
+ /**
25
+ * Creates an ObjectId from a string validating the format.
26
+ */
20
27
  make(id: string): ObjectId;
28
+
29
+ /**
30
+ * Generates a random ObjectId.
31
+ */
21
32
  random(): ObjectId;
33
+
34
+ /**
35
+ * WARNING: To be used only within tests.
36
+ *
37
+ * Disables randomness in ObjectId generation, causing the same sequence of IDs to be generated.
38
+ * Do not use in production code as this will cause data collisions.
39
+ * Place this at the top of the test file to ensure that the same sequence of IDs is generated.
40
+ *
41
+ * ```ts
42
+ * ObjectId.dangerouslyDisableRandomness();
43
+ *
44
+ * describe('suite', () => {
45
+ * // ...
46
+ * });
47
+ * ```
48
+ *
49
+ * NOTE: The generated IDs depend on the order of ObjectId.random() calls, which might be affected by test order, scheduling, etc.
50
+ */
51
+ dangerouslyDisableRandomness(): void;
22
52
  }
23
53
 
24
54
  /**
@@ -27,6 +57,9 @@ export interface ObjectIdClass extends Schema.SchemaClass<ObjectId, string> {
27
57
  * Follows ULID spec.
28
58
  */
29
59
  export const ObjectId: ObjectIdClass = class extends ObjectIdSchema {
60
+ static #factory: ULIDFactory = monotonicFactory();
61
+ static #seedTime: number | undefined = undefined;
62
+
30
63
  static isValid(id: string): id is ObjectId {
31
64
  try {
32
65
  Schema.decodeSync(ObjectId)(id);
@@ -37,6 +70,62 @@ export const ObjectId: ObjectIdClass = class extends ObjectIdSchema {
37
70
  }
38
71
 
39
72
  static random(): ObjectId {
40
- return ulid() as ObjectId;
73
+ return this.#factory(this.#seedTime) as ObjectId;
74
+ }
75
+
76
+ static dangerouslyDisableRandomness() {
77
+ this.#factory = monotonicFactory(makeTestPRNG());
78
+ this.#seedTime = new Date('2025-01-01').getTime();
41
79
  }
42
80
  };
81
+
82
+ /**
83
+ * Test PRNG that always starts with the same seed and produces the same sequence.
84
+ */
85
+ const makeTestPRNG = (): PRNG => {
86
+ const rng = new SimplePRNG();
87
+ return () => {
88
+ return rng.next();
89
+ };
90
+ };
91
+
92
+ /**
93
+ * Simple Linear Congruential Generator (LCG) for pseudo-random number generation.
94
+ * Returns numbers in the range [0, 1) (0 inclusive, 1 exclusive).
95
+ */
96
+ export class SimplePRNG {
97
+ #seed: number;
98
+
99
+ // LCG parameters (from Numerical Recipes)
100
+ static readonly #a = 1664525;
101
+ static readonly #c = 1013904223;
102
+ static readonly #m = Math.pow(2, 32);
103
+
104
+ /**
105
+ * Creates a new PRNG instance.
106
+ * @param seed - Initial seed value. If not provided, uses 0.
107
+ */
108
+ constructor(seed: number = 0) {
109
+ this.#seed = seed;
110
+ }
111
+
112
+ /**
113
+ * Generates the next pseudo-random number in the range [0, 1).
114
+ * @returns A pseudo-random number between 0 (inclusive) and 1 (exclusive).
115
+ */
116
+ next(): number {
117
+ // Update seed using LCG formula: (a * seed + c) mod m
118
+ this.#seed = (SimplePRNG.#a * this.#seed + SimplePRNG.#c) % SimplePRNG.#m;
119
+
120
+ // Normalize to [0, 1) range
121
+ return this.#seed / SimplePRNG.#m;
122
+ }
123
+
124
+ /**
125
+ * Resets the generator with a new seed.
126
+ * @param seed - New seed value.
127
+ */
128
+ reset(seed: number): void {
129
+ this.#seed = seed;
130
+ }
131
+ }
package/src/prng.ts ADDED
@@ -0,0 +1,54 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ /**
6
+ * Simple Linear Congruential Generator (LCG) for pseudo-random number generation.
7
+ * Returns numbers in the range [0, 1) (0 inclusive, 1 exclusive).
8
+ */
9
+ export class SimplePRNG {
10
+ private _seed: number;
11
+
12
+ // LCG parameters (from Numerical Recipes)
13
+ private static readonly _a = 1664525;
14
+ private static readonly _c = 1013904223;
15
+ private static readonly _m = Math.pow(2, 32);
16
+
17
+ /**
18
+ * Creates a new PRNG instance.
19
+ * @param seed - Initial seed value. If not provided, uses current timestamp.
20
+ */
21
+ constructor(seed?: number) {
22
+ this._seed = seed ?? Date.now();
23
+ }
24
+
25
+ /**
26
+ * Generates the next pseudo-random number in the range [0, 1).
27
+ * @returns A pseudo-random number between 0 (inclusive) and 1 (exclusive).
28
+ */
29
+ next(): number {
30
+ // Update seed using LCG formula: (a * seed + c) mod m
31
+ this._seed = (SimplePRNG._a * this._seed + SimplePRNG._c) % SimplePRNG._m;
32
+
33
+ // Normalize to [0, 1) range
34
+ return this._seed / SimplePRNG._m;
35
+ }
36
+
37
+ /**
38
+ * Resets the generator with a new seed.
39
+ * @param seed - New seed value.
40
+ */
41
+ reset(seed: number): void {
42
+ this._seed = seed;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Creates a simple PRNG function with optional seed.
48
+ * @param seed - Optional seed value.
49
+ * @returns A function that returns pseudo-random numbers in [0, 1).
50
+ */
51
+ export const createPRNG = (seed?: number): (() => number) => {
52
+ const prng = new SimplePRNG(seed);
53
+ return () => prng.next();
54
+ };
package/src/space-id.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import base32Decode from 'base32-decode';
6
6
  import base32Encode from 'base32-encode';
7
- import { Schema } from 'effect';
7
+ import * as Schema from 'effect/Schema';
8
8
 
9
9
  import { invariant } from '@dxos/invariant';
10
10