@leanbase.com/js 0.1.1 → 0.1.3

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.
@@ -0,0 +1,357 @@
1
+ import { isFunction, isUndefined, includes } from '@posthog/core'
2
+
3
+ /**
4
+ * this device detection code is (at time of writing) about 3% of the size of the entire library
5
+ * this is mostly because the identifiers user in regexes and results can't be minified away since
6
+ * they have meaning
7
+ *
8
+ * so, there are some pre-uglifying choices in the code to help reduce the size
9
+ * e.g. many repeated strings are stored as variables and then old-fashioned concatenated together
10
+ *
11
+ * TL;DR here be dragons
12
+ */
13
+ const FACEBOOK = 'Facebook'
14
+ const MOBILE = 'Mobile'
15
+ const IOS = 'iOS'
16
+ const ANDROID = 'Android'
17
+ const TABLET = 'Tablet'
18
+ const ANDROID_TABLET = ANDROID + ' ' + TABLET
19
+ const IPAD = 'iPad'
20
+ const APPLE = 'Apple'
21
+ const APPLE_WATCH = APPLE + ' Watch'
22
+ const SAFARI = 'Safari'
23
+ const BLACKBERRY = 'BlackBerry'
24
+ const SAMSUNG = 'Samsung'
25
+ const SAMSUNG_BROWSER = SAMSUNG + 'Browser'
26
+ const SAMSUNG_INTERNET = SAMSUNG + ' Internet'
27
+ const CHROME = 'Chrome'
28
+ const CHROME_OS = CHROME + ' OS'
29
+ const CHROME_IOS = CHROME + ' ' + IOS
30
+ const INTERNET_EXPLORER = 'Internet Explorer'
31
+ const INTERNET_EXPLORER_MOBILE = INTERNET_EXPLORER + ' ' + MOBILE
32
+ const OPERA = 'Opera'
33
+ const OPERA_MINI = OPERA + ' Mini'
34
+ const EDGE = 'Edge'
35
+ const MICROSOFT_EDGE = 'Microsoft ' + EDGE
36
+ const FIREFOX = 'Firefox'
37
+ const FIREFOX_IOS = FIREFOX + ' ' + IOS
38
+ const NINTENDO = 'Nintendo'
39
+ const PLAYSTATION = 'PlayStation'
40
+ const XBOX = 'Xbox'
41
+ const ANDROID_MOBILE = ANDROID + ' ' + MOBILE
42
+ const MOBILE_SAFARI = MOBILE + ' ' + SAFARI
43
+ const WINDOWS = 'Windows'
44
+ const WINDOWS_PHONE = WINDOWS + ' Phone'
45
+ const NOKIA = 'Nokia'
46
+ const OUYA = 'Ouya'
47
+ const GENERIC = 'Generic'
48
+ const GENERIC_MOBILE = GENERIC + ' ' + MOBILE.toLowerCase()
49
+ const GENERIC_TABLET = GENERIC + ' ' + TABLET.toLowerCase()
50
+ const KONQUEROR = 'Konqueror'
51
+
52
+ const BROWSER_VERSION_REGEX_SUFFIX = '(\\d+(\\.\\d+)?)'
53
+ const DEFAULT_BROWSER_VERSION_REGEX = new RegExp('Version/' + BROWSER_VERSION_REGEX_SUFFIX)
54
+
55
+ const XBOX_REGEX = new RegExp(XBOX, 'i')
56
+ const PLAYSTATION_REGEX = new RegExp(PLAYSTATION + ' \\w+', 'i')
57
+ const NINTENDO_REGEX = new RegExp(NINTENDO + ' \\w+', 'i')
58
+ const BLACKBERRY_REGEX = new RegExp(BLACKBERRY + '|PlayBook|BB10', 'i')
59
+
60
+ const windowsVersionMap: Record<string, string> = {
61
+ 'NT3.51': 'NT 3.11',
62
+ 'NT4.0': 'NT 4.0',
63
+ '5.0': '2000',
64
+ '5.1': 'XP',
65
+ '5.2': 'XP',
66
+ '6.0': 'Vista',
67
+ '6.1': '7',
68
+ '6.2': '8',
69
+ '6.3': '8.1',
70
+ '6.4': '10',
71
+ '10.0': '10',
72
+ }
73
+
74
+ /**
75
+ * Safari detection turns out to be complicated. For e.g. https://stackoverflow.com/a/29696509
76
+ * We can be slightly loose because some options have been ruled out (e.g. firefox on iOS)
77
+ * before this check is made
78
+ */
79
+ function isSafari(userAgent: string): boolean {
80
+ return includes(userAgent, SAFARI) && !includes(userAgent, CHROME) && !includes(userAgent, ANDROID)
81
+ }
82
+
83
+ const safariCheck = (ua: string, vendor?: string) => (vendor && includes(vendor, APPLE)) || isSafari(ua)
84
+
85
+ /**
86
+ * This function detects which browser is running this script.
87
+ * The order of the checks are important since many user agents
88
+ * include keywords used in later checks.
89
+ */
90
+ export const detectBrowser = function (user_agent: string, vendor: string | undefined): string {
91
+ vendor = vendor || '' // vendor is undefined for at least IE9
92
+
93
+ if (includes(user_agent, ' OPR/') && includes(user_agent, 'Mini')) {
94
+ return OPERA_MINI
95
+ } else if (includes(user_agent, ' OPR/')) {
96
+ return OPERA
97
+ } else if (BLACKBERRY_REGEX.test(user_agent)) {
98
+ return BLACKBERRY
99
+ } else if (includes(user_agent, 'IE' + MOBILE) || includes(user_agent, 'WPDesktop')) {
100
+ return INTERNET_EXPLORER_MOBILE
101
+ }
102
+ // https://developer.samsung.com/internet/user-agent-string-format
103
+ else if (includes(user_agent, SAMSUNG_BROWSER)) {
104
+ return SAMSUNG_INTERNET
105
+ } else if (includes(user_agent, EDGE) || includes(user_agent, 'Edg/')) {
106
+ return MICROSOFT_EDGE
107
+ } else if (includes(user_agent, 'FBIOS')) {
108
+ return FACEBOOK + ' ' + MOBILE
109
+ } else if (includes(user_agent, 'UCWEB') || includes(user_agent, 'UCBrowser')) {
110
+ return 'UC Browser'
111
+ } else if (includes(user_agent, 'CriOS')) {
112
+ return CHROME_IOS // why not just Chrome?
113
+ } else if (includes(user_agent, 'CrMo')) {
114
+ return CHROME
115
+ } else if (includes(user_agent, CHROME)) {
116
+ return CHROME
117
+ } else if (includes(user_agent, ANDROID) && includes(user_agent, SAFARI)) {
118
+ return ANDROID_MOBILE
119
+ } else if (includes(user_agent, 'FxiOS')) {
120
+ return FIREFOX_IOS
121
+ } else if (includes(user_agent.toLowerCase(), KONQUEROR.toLowerCase())) {
122
+ return KONQUEROR
123
+ } else if (safariCheck(user_agent, vendor)) {
124
+ return includes(user_agent, MOBILE) ? MOBILE_SAFARI : SAFARI
125
+ } else if (includes(user_agent, FIREFOX)) {
126
+ return FIREFOX
127
+ } else if (includes(user_agent, 'MSIE') || includes(user_agent, 'Trident/')) {
128
+ return INTERNET_EXPLORER
129
+ } else if (includes(user_agent, 'Gecko')) {
130
+ return FIREFOX
131
+ }
132
+
133
+ return ''
134
+ }
135
+
136
+ const versionRegexes: Record<string, RegExp[]> = {
137
+ [INTERNET_EXPLORER_MOBILE]: [new RegExp('rv:' + BROWSER_VERSION_REGEX_SUFFIX)],
138
+ [MICROSOFT_EDGE]: [new RegExp(EDGE + '?\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
139
+ [CHROME]: [new RegExp('(' + CHROME + '|CrMo)\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
140
+ [CHROME_IOS]: [new RegExp('CriOS\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
141
+ 'UC Browser': [new RegExp('(UCBrowser|UCWEB)\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
142
+ [SAFARI]: [DEFAULT_BROWSER_VERSION_REGEX],
143
+ [MOBILE_SAFARI]: [DEFAULT_BROWSER_VERSION_REGEX],
144
+ [OPERA]: [new RegExp('(' + OPERA + '|OPR)\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
145
+ [FIREFOX]: [new RegExp(FIREFOX + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
146
+ [FIREFOX_IOS]: [new RegExp('FxiOS\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
147
+ [KONQUEROR]: [new RegExp('Konqueror[:/]?' + BROWSER_VERSION_REGEX_SUFFIX, 'i')],
148
+ // not every blackberry user agent has the version after the name
149
+ [BLACKBERRY]: [new RegExp(BLACKBERRY + ' ' + BROWSER_VERSION_REGEX_SUFFIX), DEFAULT_BROWSER_VERSION_REGEX],
150
+ [ANDROID_MOBILE]: [new RegExp('android\\s' + BROWSER_VERSION_REGEX_SUFFIX, 'i')],
151
+ [SAMSUNG_INTERNET]: [new RegExp(SAMSUNG_BROWSER + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
152
+ [INTERNET_EXPLORER]: [new RegExp('(rv:|MSIE )' + BROWSER_VERSION_REGEX_SUFFIX)],
153
+ Mozilla: [new RegExp('rv:' + BROWSER_VERSION_REGEX_SUFFIX)],
154
+ }
155
+
156
+ /**
157
+ * This function detects which browser version is running this script,
158
+ * parsing major and minor version (e.g., 42.1). User agent strings from:
159
+ * http://www.useragentstring.com/pages/useragentstring.php
160
+ *
161
+ * `navigator.vendor` is passed in and used to help with detecting certain browsers
162
+ * NB `navigator.vendor` is deprecated and not present in every browser
163
+ */
164
+ export const detectBrowserVersion = function (userAgent: string, vendor: string | undefined): number | null {
165
+ const browser = detectBrowser(userAgent, vendor)
166
+ const regexes: RegExp[] | undefined = versionRegexes[browser as keyof typeof versionRegexes]
167
+ if (isUndefined(regexes)) {
168
+ return null
169
+ }
170
+
171
+ for (let i = 0; i < regexes.length; i++) {
172
+ const regex = regexes[i]
173
+ const matches = userAgent.match(regex)
174
+ if (matches) {
175
+ return parseFloat(matches[matches.length - 2])
176
+ }
177
+ }
178
+ return null
179
+ }
180
+
181
+ // to avoid repeating regexes or calling them twice, we have an array of matches
182
+ // the first regex that matches uses its matcher function to return the result
183
+ const osMatchers: [
184
+ RegExp,
185
+ [string, string] | ((match: RegExpMatchArray | null, user_agent: string) => [string, string]),
186
+ ][] = [
187
+ [
188
+ new RegExp(XBOX + '; ' + XBOX + ' (.*?)[);]', 'i'),
189
+ (match) => {
190
+ return [XBOX, (match && match[1]) || '']
191
+ },
192
+ ],
193
+ [new RegExp(NINTENDO, 'i'), [NINTENDO, '']],
194
+ [new RegExp(PLAYSTATION, 'i'), [PLAYSTATION, '']],
195
+ [BLACKBERRY_REGEX, [BLACKBERRY, '']],
196
+ [
197
+ new RegExp(WINDOWS, 'i'),
198
+ (_, user_agent) => {
199
+ if (/Phone/.test(user_agent) || /WPDesktop/.test(user_agent)) {
200
+ return [WINDOWS_PHONE, '']
201
+ }
202
+ // not all JS versions support negative lookbehind, so we need two checks here
203
+ if (new RegExp(MOBILE).test(user_agent) && !/IEMobile\b/.test(user_agent)) {
204
+ return [WINDOWS + ' ' + MOBILE, '']
205
+ }
206
+ const match = /Windows NT ([0-9.]+)/i.exec(user_agent)
207
+ if (match && match[1]) {
208
+ const version = match[1]
209
+ let osVersion = windowsVersionMap[version] || ''
210
+ if (/arm/i.test(user_agent)) {
211
+ osVersion = 'RT'
212
+ }
213
+ return [WINDOWS, osVersion]
214
+ }
215
+ return [WINDOWS, '']
216
+ },
217
+ ],
218
+ [
219
+ /((iPhone|iPad|iPod).*?OS (\d+)_(\d+)_?(\d+)?|iPhone)/,
220
+ (match) => {
221
+ if (match && match[3]) {
222
+ const versionParts = [match[3], match[4], match[5] || '0']
223
+ return [IOS, versionParts.join('.')]
224
+ }
225
+ return [IOS, '']
226
+ },
227
+ ],
228
+ [
229
+ /(watch.*\/(\d+\.\d+\.\d+)|watch os,(\d+\.\d+),)/i,
230
+ (match) => {
231
+ // e.g. Watch4,3/5.3.8 (16U680)
232
+ let version = ''
233
+ if (match && match.length >= 3) {
234
+ version = isUndefined(match[2]) ? match[3] : match[2]
235
+ }
236
+ return ['watchOS', version]
237
+ },
238
+ ],
239
+ [
240
+ new RegExp('(' + ANDROID + ' (\\d+)\\.(\\d+)\\.?(\\d+)?|' + ANDROID + ')', 'i'),
241
+ (match) => {
242
+ if (match && match[2]) {
243
+ const versionParts = [match[2], match[3], match[4] || '0']
244
+ return [ANDROID, versionParts.join('.')]
245
+ }
246
+ return [ANDROID, '']
247
+ },
248
+ ],
249
+ [
250
+ /Mac OS X (\d+)[_.](\d+)[_.]?(\d+)?/i,
251
+ (match) => {
252
+ const result: [string, string] = ['Mac OS X', '']
253
+ if (match && match[1]) {
254
+ const versionParts = [match[1], match[2], match[3] || '0']
255
+ result[1] = versionParts.join('.')
256
+ }
257
+ return result
258
+ },
259
+ ],
260
+ [
261
+ /Mac/i,
262
+ // mop up a few non-standard UAs that should match mac
263
+ ['Mac OS X', ''],
264
+ ],
265
+ [/CrOS/, [CHROME_OS, '']],
266
+ [/Linux|debian/i, ['Linux', '']],
267
+ ]
268
+
269
+ export const detectOS = function (user_agent: string): [string, string] {
270
+ for (let i = 0; i < osMatchers.length; i++) {
271
+ const [rgex, resultOrFn] = osMatchers[i]
272
+ const match = rgex.exec(user_agent)
273
+ const result = match && (isFunction(resultOrFn) ? resultOrFn(match, user_agent) : resultOrFn)
274
+ if (result) {
275
+ return result
276
+ }
277
+ }
278
+ return ['', '']
279
+ }
280
+
281
+ export const detectDevice = function (user_agent: string): string {
282
+ if (NINTENDO_REGEX.test(user_agent)) {
283
+ return NINTENDO
284
+ } else if (PLAYSTATION_REGEX.test(user_agent)) {
285
+ return PLAYSTATION
286
+ } else if (XBOX_REGEX.test(user_agent)) {
287
+ return XBOX
288
+ } else if (new RegExp(OUYA, 'i').test(user_agent)) {
289
+ return OUYA
290
+ } else if (new RegExp('(' + WINDOWS_PHONE + '|WPDesktop)', 'i').test(user_agent)) {
291
+ return WINDOWS_PHONE
292
+ } else if (/iPad/.test(user_agent)) {
293
+ return IPAD
294
+ } else if (/iPod/.test(user_agent)) {
295
+ return 'iPod Touch'
296
+ } else if (/iPhone/.test(user_agent)) {
297
+ return 'iPhone'
298
+ } else if (/(watch)(?: ?os[,/]|\d,\d\/)[\d.]+/i.test(user_agent)) {
299
+ return APPLE_WATCH
300
+ } else if (BLACKBERRY_REGEX.test(user_agent)) {
301
+ return BLACKBERRY
302
+ } else if (/(kobo)\s(ereader|touch)/i.test(user_agent)) {
303
+ return 'Kobo'
304
+ } else if (new RegExp(NOKIA, 'i').test(user_agent)) {
305
+ return NOKIA
306
+ } else if (
307
+ // Kindle Fire without Silk / Echo Show
308
+ /(kf[a-z]{2}wi|aeo[c-r]{2})( bui|\))/i.test(user_agent) ||
309
+ // Kindle Fire HD
310
+ /(kf[a-z]+)( bui|\)).+silk\//i.test(user_agent)
311
+ ) {
312
+ return 'Kindle Fire'
313
+ } else if (/(Android|ZTE)/i.test(user_agent)) {
314
+ if (
315
+ !new RegExp(MOBILE).test(user_agent) ||
316
+ /(9138B|TB782B|Nexus [97]|pixel c|HUAWEISHT|BTV|noble nook|smart ultra 6)/i.test(user_agent)
317
+ ) {
318
+ if (
319
+ (/pixel[\daxl ]{1,6}/i.test(user_agent) && !/pixel c/i.test(user_agent)) ||
320
+ /(huaweimed-al00|tah-|APA|SM-G92|i980|zte|U304AA)/i.test(user_agent) ||
321
+ (/lmy47v/i.test(user_agent) && !/QTAQZ3/i.test(user_agent))
322
+ ) {
323
+ return ANDROID
324
+ }
325
+ return ANDROID_TABLET
326
+ } else {
327
+ return ANDROID
328
+ }
329
+ } else if (new RegExp('(pda|' + MOBILE + ')', 'i').test(user_agent)) {
330
+ return GENERIC_MOBILE
331
+ } else if (new RegExp(TABLET, 'i').test(user_agent) && !new RegExp(TABLET + ' pc', 'i').test(user_agent)) {
332
+ return GENERIC_TABLET
333
+ } else {
334
+ return ''
335
+ }
336
+ }
337
+
338
+ export const detectDeviceType = function (user_agent: string): string {
339
+ const device = detectDevice(user_agent)
340
+ if (
341
+ device === IPAD ||
342
+ device === ANDROID_TABLET ||
343
+ device === 'Kobo' ||
344
+ device === 'Kindle Fire' ||
345
+ device === GENERIC_TABLET
346
+ ) {
347
+ return TABLET
348
+ } else if (device === NINTENDO || device === XBOX || device === PLAYSTATION || device === OUYA) {
349
+ return 'Console'
350
+ } else if (device === APPLE_WATCH) {
351
+ return 'Wearable'
352
+ } else if (device) {
353
+ return MOBILE
354
+ } else {
355
+ return 'Desktop'
356
+ }
357
+ }
package/src/uuidv7.ts ADDED
@@ -0,0 +1,268 @@
1
+ /**
2
+ * uuidv7: An experimental implementation of the proposed UUID Version 7
3
+ *
4
+ * @license Apache-2.0
5
+ * @copyright 2021-2023 LiosK
6
+ * @packageDocumentation
7
+ *
8
+ * from https://github.com/LiosK/uuidv7/blob/e501462ea3d23241de13192ceae726956f9b3b7d/src/index.ts
9
+ */
10
+
11
+ // polyfill for IE11
12
+ import { window } from './utils'
13
+
14
+ import { isNumber, isUndefined } from '@posthog/core'
15
+
16
+ if (!Math.trunc) {
17
+ Math.trunc = function (v) {
18
+ return v < 0 ? Math.ceil(v) : Math.floor(v)
19
+ }
20
+ }
21
+
22
+ // polyfill for IE11
23
+ if (!Number.isInteger) {
24
+ Number.isInteger = function (value) {
25
+ return isNumber(value) && isFinite(value) && Math.floor(value) === value
26
+ }
27
+ }
28
+
29
+ const DIGITS = '0123456789abcdef'
30
+
31
+ /** Represents a UUID as a 16-byte byte array. */
32
+ export class UUID {
33
+ /** @param bytes - The 16-byte byte array representation. */
34
+ constructor(readonly bytes: Readonly<Uint8Array>) {
35
+ if (bytes.length !== 16) {
36
+ throw new TypeError('not 128-bit length')
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Builds a byte array from UUIDv7 field values.
42
+ *
43
+ * @param unixTsMs - A 48-bit `unix_ts_ms` field value.
44
+ * @param randA - A 12-bit `rand_a` field value.
45
+ * @param randBHi - The higher 30 bits of 62-bit `rand_b` field value.
46
+ * @param randBLo - The lower 32 bits of 62-bit `rand_b` field value.
47
+ */
48
+ static fromFieldsV7(unixTsMs: number, randA: number, randBHi: number, randBLo: number): UUID {
49
+ if (
50
+ !Number.isInteger(unixTsMs) ||
51
+ !Number.isInteger(randA) ||
52
+ !Number.isInteger(randBHi) ||
53
+ !Number.isInteger(randBLo) ||
54
+ unixTsMs < 0 ||
55
+ randA < 0 ||
56
+ randBHi < 0 ||
57
+ randBLo < 0 ||
58
+ unixTsMs > 0xffff_ffff_ffff ||
59
+ randA > 0xfff ||
60
+ randBHi > 0x3fff_ffff ||
61
+ randBLo > 0xffff_ffff
62
+ ) {
63
+ throw new RangeError('invalid field value')
64
+ }
65
+
66
+ const bytes = new Uint8Array(16)
67
+ bytes[0] = unixTsMs / 2 ** 40
68
+ bytes[1] = unixTsMs / 2 ** 32
69
+ bytes[2] = unixTsMs / 2 ** 24
70
+ bytes[3] = unixTsMs / 2 ** 16
71
+ bytes[4] = unixTsMs / 2 ** 8
72
+ bytes[5] = unixTsMs
73
+ bytes[6] = 0x70 | (randA >>> 8)
74
+ bytes[7] = randA
75
+ bytes[8] = 0x80 | (randBHi >>> 24)
76
+ bytes[9] = randBHi >>> 16
77
+ bytes[10] = randBHi >>> 8
78
+ bytes[11] = randBHi
79
+ bytes[12] = randBLo >>> 24
80
+ bytes[13] = randBLo >>> 16
81
+ bytes[14] = randBLo >>> 8
82
+ bytes[15] = randBLo
83
+ return new UUID(bytes)
84
+ }
85
+
86
+ /** @returns The 8-4-4-4-12 canonical hexadecimal string representation. */
87
+ toString(): string {
88
+ let text = ''
89
+ for (let i = 0; i < this.bytes.length; i++) {
90
+ text = text + DIGITS.charAt(this.bytes[i] >>> 4) + DIGITS.charAt(this.bytes[i] & 0xf)
91
+ if (i === 3 || i === 5 || i === 7 || i === 9) {
92
+ text += '-'
93
+ }
94
+ }
95
+
96
+ if (text.length !== 36) {
97
+ // We saw one customer whose bundling code was mangling the UUID generation
98
+ // rather than accept a bad UUID, we throw an error here.
99
+ throw new Error('Invalid UUIDv7 was generated')
100
+ }
101
+ return text
102
+ }
103
+
104
+ /** Creates an object from `this`. */
105
+ clone(): UUID {
106
+ return new UUID(this.bytes.slice(0))
107
+ }
108
+
109
+ /** Returns true if `this` is equivalent to `other`. */
110
+ equals(other: UUID): boolean {
111
+ return this.compareTo(other) === 0
112
+ }
113
+
114
+ /**
115
+ * Returns a negative integer, zero, or positive integer if `this` is less
116
+ * than, equal to, or greater than `other`, respectively.
117
+ */
118
+ compareTo(other: UUID): number {
119
+ for (let i = 0; i < 16; i++) {
120
+ const diff = this.bytes[i] - other.bytes[i]
121
+ if (diff !== 0) {
122
+ return Math.sign(diff)
123
+ }
124
+ }
125
+ return 0
126
+ }
127
+ }
128
+
129
+ /** Encapsulates the monotonic counter state. */
130
+ class V7Generator {
131
+ private _timestamp = 0
132
+ private _counter = 0
133
+ private readonly _random = new DefaultRandom()
134
+
135
+ /**
136
+ * Generates a new UUIDv7 object from the current timestamp, or resets the
137
+ * generator upon significant timestamp rollback.
138
+ *
139
+ * This method returns monotonically increasing UUIDs unless the up-to-date
140
+ * timestamp is significantly (by ten seconds or more) smaller than the one
141
+ * embedded in the immediately preceding UUID. If such a significant clock
142
+ * rollback is detected, this method resets the generator and returns a new
143
+ * UUID based on the current timestamp.
144
+ */
145
+ generate(): UUID {
146
+ const value = this.generateOrAbort()
147
+ if (!isUndefined(value)) {
148
+ return value
149
+ } else {
150
+ // reset state and resume
151
+ this._timestamp = 0
152
+ const valueAfterReset = this.generateOrAbort()
153
+ if (isUndefined(valueAfterReset)) {
154
+ throw new Error('Could not generate UUID after timestamp reset')
155
+ }
156
+ return valueAfterReset
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Generates a new UUIDv7 object from the current timestamp, or returns
162
+ * `undefined` upon significant timestamp rollback.
163
+ *
164
+ * This method returns monotonically increasing UUIDs unless the up-to-date
165
+ * timestamp is significantly (by ten seconds or more) smaller than the one
166
+ * embedded in the immediately preceding UUID. If such a significant clock
167
+ * rollback is detected, this method aborts and returns `undefined`.
168
+ */
169
+ generateOrAbort(): UUID | undefined {
170
+ const MAX_COUNTER = 0x3ff_ffff_ffff
171
+ const ROLLBACK_ALLOWANCE = 10_000 // 10 seconds
172
+
173
+ const ts = Date.now()
174
+ if (ts > this._timestamp) {
175
+ this._timestamp = ts
176
+ this._resetCounter()
177
+ } else if (ts + ROLLBACK_ALLOWANCE > this._timestamp) {
178
+ // go on with previous timestamp if new one is not much smaller
179
+ this._counter++
180
+ if (this._counter > MAX_COUNTER) {
181
+ // increment timestamp at counter overflow
182
+ this._timestamp++
183
+ this._resetCounter()
184
+ }
185
+ } else {
186
+ // abort if clock went backwards to unbearable extent
187
+ return undefined
188
+ }
189
+
190
+ return UUID.fromFieldsV7(
191
+ this._timestamp,
192
+ Math.trunc(this._counter / 2 ** 30),
193
+ this._counter & (2 ** 30 - 1),
194
+ this._random.nextUint32()
195
+ )
196
+ }
197
+
198
+ /** Initializes the counter at a 42-bit random integer. */
199
+ private _resetCounter(): void {
200
+ this._counter = this._random.nextUint32() * 0x400 + (this._random.nextUint32() & 0x3ff)
201
+ }
202
+ }
203
+
204
+ /** A global flag to force use of cryptographically strong RNG. */
205
+ declare const UUIDV7_DENY_WEAK_RNG: boolean
206
+
207
+ /** Stores `crypto.getRandomValues()` available in the environment. */
208
+ let getRandomValues: <T extends Uint8Array | Uint32Array>(buffer: T) => T = (buffer) => {
209
+ // fall back on Math.random() unless the flag is set to true
210
+ // TRICKY: don't use the isUndefined method here as can't pass the reference
211
+ if (typeof UUIDV7_DENY_WEAK_RNG !== 'undefined' && UUIDV7_DENY_WEAK_RNG) {
212
+ throw new Error('no cryptographically strong RNG available')
213
+ }
214
+
215
+ for (let i = 0; i < buffer.length; i++) {
216
+ buffer[i] = Math.trunc(Math.random() * 0x1_0000) * 0x1_0000 + Math.trunc(Math.random() * 0x1_0000)
217
+ }
218
+ return buffer
219
+ }
220
+
221
+ // detect Web Crypto API
222
+ if (window && !isUndefined(window.crypto) && crypto.getRandomValues) {
223
+ getRandomValues = (buffer) => crypto.getRandomValues(buffer)
224
+ }
225
+
226
+ /**
227
+ * Wraps `crypto.getRandomValues()` and compatibles to enable buffering; this
228
+ * uses a small buffer by default to avoid unbearable throughput decline in some
229
+ * environments as well as the waste of time and space for unused values.
230
+ */
231
+ class DefaultRandom {
232
+ private readonly _buffer = new Uint32Array(8)
233
+ private _cursor = Infinity
234
+ nextUint32(): number {
235
+ if (this._cursor >= this._buffer.length) {
236
+ getRandomValues(this._buffer)
237
+ this._cursor = 0
238
+ }
239
+ return this._buffer[this._cursor++]
240
+ }
241
+ }
242
+
243
+ let defaultGenerator: V7Generator | undefined
244
+
245
+ /**
246
+ * Generates a UUIDv7 string.
247
+ *
248
+ * @returns The 8-4-4-4-12 canonical hexadecimal string representation
249
+ * ("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").
250
+ */
251
+ export const uuidv7 = (): string => uuidv7obj().toString()
252
+
253
+ /** Generates a UUIDv7 object. */
254
+ const uuidv7obj = (): UUID => (defaultGenerator || (defaultGenerator = new V7Generator())).generate()
255
+
256
+ export const uuid7ToTimestampMs = (uuid: string): number => {
257
+ // remove hyphens
258
+ const hex = uuid.replace(/-/g, '')
259
+ // ensure that it's a version 7 UUID
260
+ if (hex.length !== 32) {
261
+ throw new Error('Not a valid UUID')
262
+ }
263
+ if (hex[12] !== '7') {
264
+ throw new Error('Not a UUIDv7')
265
+ }
266
+ // the first 6 bytes are the timestamp, which means that we can read only the first 12 hex characters
267
+ return parseInt(hex.substring(0, 12), 16)
268
+ }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '0.1.1'
1
+ export const version = '0.1.3'
package/LICENSE DELETED
@@ -1,37 +0,0 @@
1
- Copyright 2025 Leanflag Limited
2
- This project is a fork of PostHog/posthog-js.
3
-
4
- All rights reserved. This software is proprietary to Leanflag Limited and may only be used for Leanflag Limited products and internal purposes. Redistribution, modification, or commercial use outside of Leanflag Limited is strictly prohibited without explicit written permission from Leanflag Limited.
5
-
6
-
7
- ---
8
-
9
- Copyright 2020 Posthog / Hiberly, Inc.
10
-
11
- Copyright 2015 Mixpanel, Inc.
12
-
13
-
14
- Some files in this codebase contain code from getsentry/sentry-javascript by Software, Inc. dba Sentry.
15
- In such cases it is explicitly stated in the file header. This license only applies to the relevant code in such cases.
16
-
17
- MIT License
18
-
19
- Copyright (c) 2012 Functional Software, Inc. dba Sentry
20
-
21
- Permission is hereby granted, free of charge, to any person obtaining a copy of
22
- this software and associated documentation files (the "Software"), to deal in
23
- the Software without restriction, including without limitation the rights to
24
- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
25
- of the Software, and to permit persons to whom the Software is furnished to do
26
- so, subject to the following conditions:
27
-
28
- The above copyright notice and this permission notice shall be included in all
29
- copies or substantial portions of the Software.
30
-
31
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
37
- SOFTWARE.