Skip to content

Commit 3a73cf0

Browse files
feat(idgen): update IDGen, add MonotonicID, PrefixedID
- restructure package - add/update new impls
1 parent cbdc527 commit 3a73cf0

File tree

6 files changed

+357
-214
lines changed

6 files changed

+357
-214
lines changed

packages/idgen/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,18 @@
6868
"exports": {
6969
".": {
7070
"default": "./index.js"
71+
},
72+
"./api": {
73+
"default": "./api.js"
74+
},
75+
"./idgen": {
76+
"default": "./idgen.js"
77+
},
78+
"./monotonic": {
79+
"default": "./monotonic.js"
80+
},
81+
"./prefixed": {
82+
"default": "./prefixed.js"
7183
}
7284
},
7385
"thi.ng": {

packages/idgen/src/api.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { EVENT_ALL } from "@thi.ng/api";
2+
3+
export const EVENT_ADDED = "added";
4+
export const EVENT_REMOVED = "removed";
5+
6+
export type IDGenEventType =
7+
| typeof EVENT_ADDED
8+
| typeof EVENT_REMOVED
9+
| typeof EVENT_ALL;

packages/idgen/src/idgen.ts

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import type { Event, IClear, IIDGen, INotify, Listener } from "@thi.ng/api";
2+
import { INotifyMixin } from "@thi.ng/api/mixins/inotify";
3+
import { assert } from "@thi.ng/errors/assert";
4+
import { EVENT_ADDED, EVENT_REMOVED, type IDGenEventType } from "./api.js";
5+
6+
@INotifyMixin
7+
export class IDGen
8+
implements
9+
Iterable<number>,
10+
IClear,
11+
IIDGen<number>,
12+
INotify<IDGenEventType>
13+
{
14+
readonly ids: number[];
15+
16+
protected nextID: number;
17+
protected _freeID: number;
18+
protected start: number;
19+
protected num: number;
20+
protected _capacity: number;
21+
protected mask: number;
22+
protected vmask: number;
23+
protected shift: number;
24+
25+
constructor(bits = 32, vbits = 32 - bits, cap = 2 ** bits, start = 0) {
26+
const maxCap = 2 ** bits;
27+
assert(bits > 0 && bits + vbits <= 32, "wrong total bit size [1..32]");
28+
assert(
29+
cap <= maxCap,
30+
`requested capacity too large for bit size (max. ${maxCap})`
31+
);
32+
this.ids = [];
33+
this.nextID = start;
34+
this.start = start;
35+
this._capacity = cap;
36+
this.num = 0;
37+
this.mask = maxCap - 1;
38+
this.vmask = (1 << vbits) - 1;
39+
this.shift = bits;
40+
this._freeID = -1;
41+
}
42+
43+
/**
44+
* Extract actual ID (without version bits).
45+
*
46+
* @param id -
47+
*/
48+
id(id: number) {
49+
return id & this.mask;
50+
}
51+
52+
/**
53+
* Extract version from ID
54+
*
55+
* @param id -
56+
*/
57+
version(id: number) {
58+
return (id >>> this.shift) & this.vmask;
59+
}
60+
61+
get capacity() {
62+
return this._capacity;
63+
}
64+
65+
/**
66+
* Attempts to set new capacity to given value. Capacity can only be
67+
* increased and the operation is only supported for unversioned
68+
* instances (i.e. vbits = 0).
69+
*/
70+
set capacity(newCap: number) {
71+
assert(!this.vmask, "can't change capacity w/ versioning enabled");
72+
if (newCap >= this.mask + 1) {
73+
const bits = Math.ceil(Math.log2(newCap));
74+
assert(
75+
bits > 0 && bits <= 32,
76+
"wrong bit size for new capacity [1..32]"
77+
);
78+
this._capacity = newCap;
79+
this.mask = 2 ** bits - 1;
80+
this.shift = bits;
81+
} else {
82+
throw new Error("can't reduce capacity");
83+
}
84+
}
85+
86+
/**
87+
* Number of remaining available IDs.
88+
*/
89+
get available() {
90+
return this._capacity - this.num - this.start;
91+
}
92+
93+
/**
94+
* Number of currently used IDs.
95+
*/
96+
get used() {
97+
return this.num;
98+
}
99+
100+
/**
101+
* Next available free ID.
102+
*/
103+
get freeID() {
104+
return this._freeID;
105+
}
106+
107+
*[Symbol.iterator]() {
108+
const { ids, mask } = this;
109+
for (let i = this.nextID; i-- > 0; ) {
110+
const id = ids[i];
111+
if ((id & mask) === i) yield id;
112+
}
113+
}
114+
115+
/**
116+
* Frees all existing IDs and resets counter to original start ID.
117+
*/
118+
clear() {
119+
this.ids.length = 0;
120+
this.nextID = this.start;
121+
this.num = 0;
122+
this._freeID = -1;
123+
}
124+
125+
/**
126+
* Returns next available ID or throws error (assertion) if no further IDs
127+
* are currently available. Emits {@link EVENT_ADDED} if successful.
128+
*/
129+
next() {
130+
let id: number;
131+
if (this._freeID !== -1) {
132+
id = this._freeID;
133+
const rawID = id & this.mask;
134+
this._freeID = this.ids[rawID];
135+
this.ids[rawID] = id;
136+
} else {
137+
assert(this.nextID < this._capacity, "max capacity reached");
138+
id = this.nextID++;
139+
this.ids[id] = id;
140+
}
141+
this.num++;
142+
this.notify({ id: EVENT_ADDED, target: this, value: id });
143+
return id;
144+
}
145+
146+
/**
147+
* Marks given ID as available again and increases its version (if
148+
* versioning is enabled). Emits {@link EVENT_REMOVED} if successful.
149+
*
150+
* @param id -
151+
*/
152+
free(id: number) {
153+
if (!this.has(id)) return false;
154+
this.ids[id & this.mask] = this._freeID;
155+
this._freeID = this.nextVersion(id);
156+
this.num--;
157+
this.notify({ id: EVENT_REMOVED, target: this, value: id });
158+
return true;
159+
}
160+
161+
/**
162+
* Returns true iff the given ID is valid and currently used.
163+
*
164+
* @param id -
165+
*/
166+
has(id: number) {
167+
const rawID = id & this.mask;
168+
return id >= 0 && rawID < this.nextID && this.ids[rawID] === id;
169+
}
170+
171+
/** {@inheritDoc @thi.ng/api#INotify.addListener} */
172+
// @ts-ignore: mixin
173+
// prettier-ignore
174+
addListener(id: IDGenEventType, fn: Listener<IDGenEventType>, scope?: any): boolean {}
175+
176+
/** {@inheritDoc @thi.ng/api#INotify.removeListener} */
177+
// @ts-ignore: mixin
178+
// prettier-ignore
179+
removeListener(id: IDGenEventType, fn: Listener<IDGenEventType>, scope?: any): boolean {}
180+
181+
/** {@inheritDoc @thi.ng/api#INotify.notify} */
182+
// @ts-ignore: mixin
183+
notify(event: Event<IDGenEventType>): boolean {}
184+
185+
protected nextVersion(id: number) {
186+
return (
187+
((id & this.mask) |
188+
(((this.version(id) + 1) & this.vmask) << this.shift)) >>>
189+
0
190+
);
191+
}
192+
}
193+
194+
/**
195+
* Returns a new {@link IDGen} instance configured to use given counter &
196+
* version bits.
197+
*
198+
* @remarks
199+
* Overall ID range/capacity can be explicitly limited using `cap` (default and
200+
* maximum: 2^bits). The start ID can be defined via `start` (default: 0) and
201+
* MUST be < `cap`.
202+
*
203+
* @param bits -
204+
* @param vbits -
205+
* @param cap -
206+
* @param start -
207+
*/
208+
export const idgen = (
209+
bits?: number,
210+
vbits?: number,
211+
cap?: number,
212+
start?: number
213+
) => new IDGen(bits, vbits, cap, start);

0 commit comments

Comments
 (0)