@dxos/keys 0.8.4-staging.ac66bdf99f → 0.9.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/LICENSE +102 -5
- package/dist/lib/browser/index.mjs +294 -435
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +289 -433
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/DXN.d.ts +50 -0
- package/dist/types/src/DXN.d.ts.map +1 -0
- package/dist/types/src/DXN.test.d.ts +2 -0
- package/dist/types/src/DXN.test.d.ts.map +1 -0
- package/dist/types/src/EID.d.ts +74 -0
- package/dist/types/src/EID.d.ts.map +1 -0
- package/dist/types/src/EID.test.d.ts +2 -0
- package/dist/types/src/EID.test.d.ts.map +1 -0
- package/dist/types/src/URI.d.ts +23 -0
- package/dist/types/src/URI.d.ts.map +1 -0
- package/dist/types/src/entity-id.d.ts +104 -0
- package/dist/types/src/entity-id.d.ts.map +1 -0
- package/dist/types/src/entity-id.test.d.ts +2 -0
- package/dist/types/src/entity-id.test.d.ts.map +1 -0
- package/dist/types/src/identity-did.d.ts +6 -4
- package/dist/types/src/identity-did.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +5 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/parse-id.d.ts +10 -0
- package/dist/types/src/parse-id.d.ts.map +1 -0
- package/dist/types/src/parse-id.test.d.ts +2 -0
- package/dist/types/src/parse-id.test.d.ts.map +1 -0
- package/dist/types/src/prng.d.ts.map +1 -1
- package/dist/types/src/public-key.d.ts +1 -1
- package/dist/types/src/public-key.d.ts.map +1 -1
- package/dist/types/src/random-bytes.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -10
- package/src/DXN.test.ts +85 -0
- package/src/DXN.ts +104 -0
- package/src/EID.test.ts +147 -0
- package/src/EID.ts +151 -0
- package/src/URI.ts +35 -0
- package/src/entity-id.test.ts +73 -0
- package/src/entity-id.ts +202 -0
- package/src/identity-did.test.ts +19 -2
- package/src/identity-did.ts +60 -25
- package/src/index.ts +6 -2
- package/src/parse-id.test.ts +32 -0
- package/src/parse-id.ts +32 -0
- package/src/public-key.ts +3 -3
- package/dist/types/src/dxn.d.ts +0 -129
- package/dist/types/src/dxn.d.ts.map +0 -1
- package/dist/types/src/object-id.d.ts +0 -65
- package/dist/types/src/object-id.d.ts.map +0 -1
- package/src/dxn.ts +0 -345
- package/src/object-id.ts +0 -131
package/src/dxn.ts
DELETED
|
@@ -1,345 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import * as Schema from 'effect/Schema';
|
|
6
|
-
import type { InspectOptionsStylized, inspect } from 'node:util';
|
|
7
|
-
|
|
8
|
-
import { type DevtoolsFormatter, devtoolsFormatter, inspectCustom } from '@dxos/debug';
|
|
9
|
-
import { assertArgument, invariant } from '@dxos/invariant';
|
|
10
|
-
|
|
11
|
-
import { ObjectId } from './object-id';
|
|
12
|
-
import { SpaceId } from './space-id';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Tags for ECHO DXNs that should resolve the object ID in the local space.
|
|
16
|
-
*/
|
|
17
|
-
// TODO(dmaretskyi): Rebrand this as "unknown location" to specify objects in the same space or queue. Essentially making the DXN it a URI not URL
|
|
18
|
-
// TODO(dmaretskyi): "@" is a separator character in the URI spec.
|
|
19
|
-
export const LOCAL_SPACE_TAG = '@';
|
|
20
|
-
|
|
21
|
-
export const DXN_ECHO_REGEXP = /@(dxn:[a-zA-Z0-p:@]+)/;
|
|
22
|
-
|
|
23
|
-
// TODO(burdon): Namespace for.
|
|
24
|
-
export const QueueSubspaceTags = Object.freeze({
|
|
25
|
-
DATA: 'data',
|
|
26
|
-
TRACE: 'trace',
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
export type QueueSubspaceTag = (typeof QueueSubspaceTags)[keyof typeof QueueSubspaceTags];
|
|
30
|
-
|
|
31
|
-
// TODO(burdon): Refactor.
|
|
32
|
-
// Consider: https://github.com/multiformats/multiaddr
|
|
33
|
-
// dxn:echo:[<space-id>:[<queue-id>:]]<object-id>
|
|
34
|
-
// dxn:echo:[S/<space-id>:[Q/<queue-id>:]]<object-id>
|
|
35
|
-
// dxn:type:org.dxos.markdown.contact
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* DXN unambiguously names a resource like an ECHO object, schema definition, plugin, etc.
|
|
39
|
-
* Each DXN starts with a dxn prefix, followed by a resource kind.
|
|
40
|
-
* Colon Symbol : is used a delimiter between parts.
|
|
41
|
-
* DXNs may contain slashes.
|
|
42
|
-
* '@' in the place of the space id is used to denote that the DXN should be resolved in the local space.
|
|
43
|
-
*
|
|
44
|
-
* @example
|
|
45
|
-
* ```
|
|
46
|
-
* dxn:echo:<space key>:<echo id>
|
|
47
|
-
* dxn:echo:BA25QRC2FEWCSAMRP4RZL65LWJ7352CKE:01J00J9B45YHYSGZQTQMSKMGJ6
|
|
48
|
-
* dxn:echo:@:01J00J9B45YHYSGZQTQMSKMGJ6
|
|
49
|
-
* dxn:type:org.dxos.type.calendar
|
|
50
|
-
* dxn:plugin:org.dxos.agent.plugin.functions
|
|
51
|
-
* ```
|
|
52
|
-
*/
|
|
53
|
-
export class DXN {
|
|
54
|
-
// TODO(burdon): Rename to DXN (i.e., DXN.DXN).
|
|
55
|
-
// TODO(dmaretskyi): Should this be a transformation into the DXN type?
|
|
56
|
-
static Schema = Schema.NonEmptyString.pipe(
|
|
57
|
-
Schema.pattern(/^dxn:([^:]+):(?:[^:]+:?)+[^:]$/),
|
|
58
|
-
// TODO(dmaretskyi): To set the format we need to move the annotation IDs out of the echo-schema package.
|
|
59
|
-
// FormatAnnotation.set(TypeFormat.DXN),
|
|
60
|
-
Schema.annotations({
|
|
61
|
-
title: 'DXN',
|
|
62
|
-
description: 'DXN URI',
|
|
63
|
-
examples: ['dxn:type:com.example.type.my-type', 'dxn:echo:@:01J00J9B45YHYSGZQTQMSKMGJ6'],
|
|
64
|
-
}),
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
static hash(dxn: DXN): string {
|
|
68
|
-
return dxn.toString();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Kind constants.
|
|
73
|
-
*/
|
|
74
|
-
static kind = Object.freeze({
|
|
75
|
-
/**
|
|
76
|
-
* dxn:type:<type_name>[:<version>]
|
|
77
|
-
*/
|
|
78
|
-
TYPE: 'type',
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* dxn:echo:<space_id>:<echo_id>
|
|
82
|
-
* dxn:echo:@:<echo_id>
|
|
83
|
-
*/
|
|
84
|
-
// TODO(burdon): Rename to OBJECT? (BREAKING CHANGE to update "echo").
|
|
85
|
-
// TODO(burdon): Add separate Kind for space?
|
|
86
|
-
ECHO: 'echo',
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* The subspace tag enables us to partition queues by usage within the context of a space.
|
|
90
|
-
* dxn:queue:<subspace_tag>:<space_id>:<queue_id>[:object_id]
|
|
91
|
-
* dxn:queue:data:BA25QRC2FEWCSAMRP4RZL65LWJ7352CKE:01J00J9B45YHYSGZQTQMSKMGJ6
|
|
92
|
-
* dxn:queue:trace:BA25QRC2FEWCSAMRP4RZL65LWJ7352CKE:01J00J9B45YHYSGZQTQMSKMGJ6
|
|
93
|
-
*/
|
|
94
|
-
QUEUE: 'queue',
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Exactly equals.
|
|
99
|
-
*/
|
|
100
|
-
static equals(a: DXN, b: DXN): boolean {
|
|
101
|
-
return a.kind === b.kind && a.parts.length === b.parts.length && a.parts.every((part, i) => part === b.parts[i]);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
static equalsEchoId(a: DXN, b: DXN): boolean {
|
|
105
|
-
const a1 = a.asEchoDXN();
|
|
106
|
-
const b1 = b.asEchoDXN();
|
|
107
|
-
return !!a1 && !!b1 && a1.echoId === b1.echoId;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// TODO(burdon): Rename isValid.
|
|
111
|
-
static isDXNString(dxn: string): boolean {
|
|
112
|
-
return dxn.startsWith('dxn:');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
static parse(dxn: string): DXN {
|
|
116
|
-
if (typeof dxn !== 'string') {
|
|
117
|
-
throw new Error(`Invalid DXN: ${dxn}`);
|
|
118
|
-
}
|
|
119
|
-
const [prefix, kind, ...parts] = dxn.split(':');
|
|
120
|
-
if (!(prefix === 'dxn')) {
|
|
121
|
-
throw new Error(`Invalid DXN: ${dxn}`);
|
|
122
|
-
}
|
|
123
|
-
if (!(typeof kind === 'string' && kind.length > 0)) {
|
|
124
|
-
throw new Error(`Invalid DXN: ${dxn}`);
|
|
125
|
-
}
|
|
126
|
-
if (!(parts.length > 0)) {
|
|
127
|
-
throw new Error(`Invalid DXN: ${dxn}`);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return new DXN(kind, parts);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
static tryParse(dxn: string): DXN | undefined {
|
|
134
|
-
try {
|
|
135
|
-
return DXN.parse(dxn);
|
|
136
|
-
} catch {
|
|
137
|
-
return undefined;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* @example `dxn:type:com.example.type.person`
|
|
143
|
-
*/
|
|
144
|
-
static fromTypename(typename: string): DXN {
|
|
145
|
-
return new DXN(DXN.kind.TYPE, [typename]);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* @example `dxn:type:com.example.type.person:0.1.0`
|
|
150
|
-
*/
|
|
151
|
-
// TODO(dmaretskyi): Consider using @ as the version separator.
|
|
152
|
-
static fromTypenameAndVersion(typename: string, version: string): DXN {
|
|
153
|
-
return new DXN(DXN.kind.TYPE, [typename, version]);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* @example `dxn:echo:BA25QRC2FEWCSAMRP4RZL65LWJ7352CKE:01J00J9B45YHYSGZQTQMSKMGJ6`
|
|
158
|
-
*/
|
|
159
|
-
static fromSpaceAndObjectId(spaceId: SpaceId, objectId: ObjectId): DXN {
|
|
160
|
-
assertArgument(SpaceId.isValid(spaceId), `Invalid space ID: ${spaceId}`);
|
|
161
|
-
assertArgument(ObjectId.isValid(objectId), 'objectId', `Invalid object ID: ${objectId}`);
|
|
162
|
-
return new DXN(DXN.kind.ECHO, [spaceId, objectId]);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* @example `dxn:echo:@:01J00J9B45YHYSGZQTQMSKMGJ6`
|
|
167
|
-
*/
|
|
168
|
-
static fromLocalObjectId(id: string): DXN {
|
|
169
|
-
assertArgument(ObjectId.isValid(id), 'id', `Invalid object ID: ${id}`);
|
|
170
|
-
return new DXN(DXN.kind.ECHO, [LOCAL_SPACE_TAG, id]);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
static fromQueue(subspaceTag: QueueSubspaceTag, spaceId: SpaceId, queueId: ObjectId, objectId?: ObjectId) {
|
|
174
|
-
assertArgument(SpaceId.isValid(spaceId), `Invalid space ID: ${spaceId}`);
|
|
175
|
-
assertArgument(ObjectId.isValid(queueId), 'queueId', `Invalid queue ID: ${queueId}`);
|
|
176
|
-
assertArgument(!objectId || ObjectId.isValid(objectId), 'objectId', `Invalid object ID: ${objectId}`);
|
|
177
|
-
|
|
178
|
-
return new DXN(DXN.kind.QUEUE, [subspaceTag, spaceId, queueId, ...(objectId ? [objectId] : [])]);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
#kind: string;
|
|
182
|
-
#parts: string[];
|
|
183
|
-
|
|
184
|
-
constructor(kind: string, parts: string[]) {
|
|
185
|
-
assertArgument(parts.length > 0, 'parts', `Invalid DXN: ${parts}`);
|
|
186
|
-
assertArgument(
|
|
187
|
-
parts.every((part) => typeof part === 'string' && part.length > 0 && part.indexOf(':') === -1),
|
|
188
|
-
'parts',
|
|
189
|
-
`Invalid DXN: ${parts}`,
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
// Per-type validation.
|
|
193
|
-
switch (kind) {
|
|
194
|
-
case DXN.kind.TYPE:
|
|
195
|
-
if (parts.length > 2) {
|
|
196
|
-
throw new Error('Invalid DXN.kind.TYPE');
|
|
197
|
-
}
|
|
198
|
-
break;
|
|
199
|
-
case DXN.kind.ECHO:
|
|
200
|
-
if (parts.length !== 2) {
|
|
201
|
-
throw new Error('Invalid DXN.kind.ECHO');
|
|
202
|
-
}
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
this.#kind = kind;
|
|
207
|
-
this.#parts = parts;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
toString(): DXN.String {
|
|
211
|
-
return `dxn:${this.#kind}:${this.#parts.join(':')}` as DXN.String;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
toJSON(): string {
|
|
215
|
-
return this.toString();
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Used by Node.js to get textual representation of this object when it's printed with a `console.log` statement.
|
|
220
|
-
*/
|
|
221
|
-
[inspectCustom](depth: number, options: InspectOptionsStylized, inspectFn: typeof inspect): string {
|
|
222
|
-
const printControlCode = (code: number) => {
|
|
223
|
-
return `\x1b[${code}m`;
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
return (
|
|
227
|
-
printControlCode(inspectFn.colors.blueBright![0]) + this.toString() + printControlCode(inspectFn.colors.reset![0])
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
get [devtoolsFormatter](): DevtoolsFormatter {
|
|
232
|
-
return {
|
|
233
|
-
header: () => {
|
|
234
|
-
return ['span', { style: 'font-weight: bold;' }, this.toString()];
|
|
235
|
-
},
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
get kind() {
|
|
240
|
-
return this.#kind;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
get parts() {
|
|
244
|
-
return this.#parts;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// TODO(burdon): Should getters fail?
|
|
248
|
-
get typename() {
|
|
249
|
-
invariant(this.#kind === DXN.kind.TYPE);
|
|
250
|
-
return this.#parts[0];
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
equals(other: DXN): boolean {
|
|
254
|
-
return DXN.equals(this, other);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
hasTypenameOf(typename: string): boolean {
|
|
258
|
-
return this.#kind === DXN.kind.TYPE && this.#parts.length === 1 && this.#parts[0] === typename;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
isLocalObjectId(): boolean {
|
|
262
|
-
return this.#kind === DXN.kind.ECHO && this.#parts[0] === LOCAL_SPACE_TAG && this.#parts.length === 2;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
asTypeDXN(): DXN.TypeDXN | undefined {
|
|
266
|
-
if (this.kind !== DXN.kind.TYPE) {
|
|
267
|
-
return undefined;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const [type, version] = this.#parts;
|
|
271
|
-
return {
|
|
272
|
-
// TODO(wittjosiah): Should be `typename` for consistency.
|
|
273
|
-
type,
|
|
274
|
-
version: version as string | undefined,
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
asEchoDXN(): DXN.EchoDXN | undefined {
|
|
279
|
-
if (this.kind !== DXN.kind.ECHO) {
|
|
280
|
-
return undefined;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const [spaceId, echoId] = this.#parts;
|
|
284
|
-
return {
|
|
285
|
-
spaceId: spaceId === LOCAL_SPACE_TAG ? undefined : (spaceId as SpaceId | undefined),
|
|
286
|
-
// TODO(burdon): objectId.
|
|
287
|
-
echoId,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
asQueueDXN(): DXN.QueueDXN | undefined {
|
|
292
|
-
if (this.kind !== DXN.kind.QUEUE) {
|
|
293
|
-
return undefined;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const [subspaceTag, spaceId, queueId, objectId] = this.#parts;
|
|
297
|
-
if (typeof queueId !== 'string') {
|
|
298
|
-
return undefined;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
return {
|
|
302
|
-
subspaceTag: subspaceTag as QueueSubspaceTag,
|
|
303
|
-
spaceId: spaceId as SpaceId,
|
|
304
|
-
queueId,
|
|
305
|
-
objectId: objectId as string | undefined,
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Produces a new DXN with the given parts appended.
|
|
311
|
-
*/
|
|
312
|
-
extend(parts: string[]): DXN {
|
|
313
|
-
return new DXN(this.#kind, [...this.#parts, ...parts]);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* API namespace.
|
|
319
|
-
*/
|
|
320
|
-
export declare namespace DXN {
|
|
321
|
-
/**
|
|
322
|
-
* DXN represented as a javascript string.
|
|
323
|
-
*/
|
|
324
|
-
// TODO(burdon): Use Effect branded string?
|
|
325
|
-
// export const String = S.String.pipe(S.brand('DXN'));
|
|
326
|
-
// export type String = S.To(typoeof String);
|
|
327
|
-
export type String = string & { __DXNString: never };
|
|
328
|
-
|
|
329
|
-
export type TypeDXN = {
|
|
330
|
-
type: string;
|
|
331
|
-
version?: string;
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
export type EchoDXN = {
|
|
335
|
-
spaceId?: SpaceId;
|
|
336
|
-
echoId: string; // TODO(dmaretskyi): Rename to `objectId` and use `ObjectId` for the type.
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
export type QueueDXN = {
|
|
340
|
-
subspaceTag: QueueSubspaceTag;
|
|
341
|
-
spaceId: SpaceId;
|
|
342
|
-
queueId: string; // TODO(dmaretskyi): ObjectId.
|
|
343
|
-
objectId?: string; // TODO(dmaretskyi): ObjectId.
|
|
344
|
-
};
|
|
345
|
-
}
|
package/src/object-id.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2025 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import * as Schema from 'effect/Schema';
|
|
6
|
-
import { type PRNG, type ULIDFactory, monotonicFactory } from 'ulidx';
|
|
7
|
-
|
|
8
|
-
// TODO(dmaretskyi): Make brand.
|
|
9
|
-
// export const ObjectIdBrand: unique symbol = Symbol('@dxos/echo/ObjectId');
|
|
10
|
-
// export const ObjectIdSchema = Schema.ULID.pipe(S.brand(ObjectIdBrand));
|
|
11
|
-
const ObjectIdSchema = Schema.String.pipe(Schema.pattern(/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/i)).annotations({
|
|
12
|
-
description: 'A Universally Unique Lexicographically Sortable Identifier',
|
|
13
|
-
pattern: '^[0-7][0-9A-HJKMNP-TV-Z]{25}$',
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
export type ObjectId = typeof ObjectIdSchema.Type;
|
|
17
|
-
|
|
18
|
-
export interface ObjectIdClass extends Schema.SchemaClass<ObjectId, string> {
|
|
19
|
-
/**
|
|
20
|
-
* @returns true if the string is a valid ObjectId.
|
|
21
|
-
*/
|
|
22
|
-
isValid(id: string): id is ObjectId;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Creates an ObjectId from a string validating the format.
|
|
26
|
-
*/
|
|
27
|
-
make(id: string): ObjectId;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Generates a random ObjectId.
|
|
31
|
-
*/
|
|
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;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Randomly generated unique identifier for an object.
|
|
56
|
-
*
|
|
57
|
-
* Follows ULID spec.
|
|
58
|
-
*/
|
|
59
|
-
export const ObjectId: ObjectIdClass = class extends ObjectIdSchema {
|
|
60
|
-
static #factory: ULIDFactory = monotonicFactory();
|
|
61
|
-
static #seedTime: number | undefined = undefined;
|
|
62
|
-
|
|
63
|
-
static isValid(id: string): id is ObjectId {
|
|
64
|
-
try {
|
|
65
|
-
Schema.decodeSync(ObjectId)(id);
|
|
66
|
-
return true;
|
|
67
|
-
} catch {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
static random(): 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();
|
|
79
|
-
}
|
|
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
|
-
}
|