git-stack-cli 2.3.1 → 2.4.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/dist/js/index.js +65 -68
- package/package.json +1 -1
- package/src/app/Exit.tsx +28 -28
- package/src/components/ErrorBoundary.tsx +4 -1
- package/src/core/short_id.test.ts +30 -0
- package/src/core/short_id.ts +51 -68
package/dist/js/index.js
CHANGED
|
@@ -32698,34 +32698,34 @@ async function sleep(time) {
|
|
|
32698
32698
|
// src/app/Exit.tsx
|
|
32699
32699
|
function Exit(props) {
|
|
32700
32700
|
React14.useEffect(() => {
|
|
32701
|
-
handle_exit().catch((err) => {
|
|
32701
|
+
Exit.handle_exit(props).catch((err) => {
|
|
32702
32702
|
console.error(err);
|
|
32703
32703
|
});
|
|
32704
|
-
async function handle_exit() {
|
|
32705
|
-
const state = Store.getState();
|
|
32706
|
-
const actions = state.actions;
|
|
32707
|
-
actions.debug(`[Exit] handle_exit ${JSON.stringify(props)}`);
|
|
32708
|
-
let exit_code = props.code;
|
|
32709
|
-
if (state.abort_handler) {
|
|
32710
|
-
exit_code = await state.abort_handler();
|
|
32711
|
-
}
|
|
32712
|
-
if (state.is_dirty_check_stash) {
|
|
32713
|
-
await cli("git stash pop");
|
|
32714
|
-
actions.output(/* @__PURE__ */ React14.createElement(Text, {
|
|
32715
|
-
color: colors.green
|
|
32716
|
-
}, "✅ Changes restored from stash"));
|
|
32717
|
-
}
|
|
32718
|
-
await sleep(1);
|
|
32719
|
-
if (props.clear) {
|
|
32720
|
-
actions.clear();
|
|
32721
|
-
}
|
|
32722
|
-
actions.unmount();
|
|
32723
|
-
process.exitCode = exit_code;
|
|
32724
|
-
process.exit();
|
|
32725
|
-
}
|
|
32726
32704
|
}, [props.clear, props.code]);
|
|
32727
32705
|
return null;
|
|
32728
32706
|
}
|
|
32707
|
+
Exit.handle_exit = async function handle_exit(props) {
|
|
32708
|
+
const state = Store.getState();
|
|
32709
|
+
const actions = state.actions;
|
|
32710
|
+
actions.debug(`[Exit] handle_exit ${JSON.stringify(props)}`);
|
|
32711
|
+
let exit_code = props.code;
|
|
32712
|
+
if (state.abort_handler) {
|
|
32713
|
+
exit_code = await state.abort_handler();
|
|
32714
|
+
}
|
|
32715
|
+
if (state.is_dirty_check_stash) {
|
|
32716
|
+
await cli("git stash pop");
|
|
32717
|
+
actions.output(/* @__PURE__ */ React14.createElement(Text, {
|
|
32718
|
+
color: colors.green
|
|
32719
|
+
}, "✅ Changes restored from stash"));
|
|
32720
|
+
}
|
|
32721
|
+
await sleep(1);
|
|
32722
|
+
if (props.clear) {
|
|
32723
|
+
actions.clear();
|
|
32724
|
+
}
|
|
32725
|
+
actions.unmount();
|
|
32726
|
+
process.exitCode = exit_code;
|
|
32727
|
+
process.exit();
|
|
32728
|
+
};
|
|
32729
32729
|
|
|
32730
32730
|
// src/app/LogTimestamp.tsx
|
|
32731
32731
|
var React15 = __toESM(require_react(), 1);
|
|
@@ -39153,44 +39153,40 @@ async function run4() {
|
|
|
39153
39153
|
// src/core/short_id.ts
|
|
39154
39154
|
import crypto2 from "node:crypto";
|
|
39155
39155
|
function short_id() {
|
|
39156
|
-
const timestamp = Date.now();
|
|
39157
|
-
const
|
|
39158
|
-
const
|
|
39159
|
-
const
|
|
39160
|
-
|
|
39161
|
-
|
|
39162
|
-
|
|
39163
|
-
}
|
|
39164
|
-
function binary(value) {
|
|
39165
|
-
return BigInt(value).toString(2);
|
|
39166
|
-
}
|
|
39167
|
-
function rand_index(list) {
|
|
39168
|
-
return Math.floor(Math.random() * list.length);
|
|
39169
|
-
}
|
|
39170
|
-
function interleave_bits(a, b2) {
|
|
39171
|
-
const a_binary = binary(a).split("");
|
|
39172
|
-
const b_binary = binary(b2).split("");
|
|
39173
|
-
while (b_binary.length) {
|
|
39174
|
-
const b_index = rand_index(b_binary);
|
|
39175
|
-
const [selected] = b_binary.splice(b_index, 1);
|
|
39176
|
-
const a_index = rand_index(a_binary);
|
|
39177
|
-
a_binary.splice(a_index, 0, selected);
|
|
39178
|
-
}
|
|
39179
|
-
const a_value = parseInt(a_binary.join(""), 2);
|
|
39180
|
-
return a_value;
|
|
39181
|
-
}
|
|
39182
|
-
function encode(value) {
|
|
39183
|
-
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-+";
|
|
39184
|
-
const bits_per_char = Math.log2(chars.length);
|
|
39185
|
-
const max_value_bits = 53;
|
|
39186
|
-
const max_char_size = Math.ceil(max_value_bits / bits_per_char);
|
|
39156
|
+
const timestamp = BigInt(Date.now());
|
|
39157
|
+
const random = BigInt(crypto2.randomInt(0, 1 << RANDOM_BITS));
|
|
39158
|
+
const combined = timestamp << BigInt(RANDOM_BITS) | random;
|
|
39159
|
+
const id = encode(combined, ID_LENGTH);
|
|
39160
|
+
return id;
|
|
39161
|
+
}
|
|
39162
|
+
function encode(value, length) {
|
|
39187
39163
|
let result = "";
|
|
39188
|
-
|
|
39189
|
-
|
|
39190
|
-
|
|
39164
|
+
if (value > 0n) {
|
|
39165
|
+
while (value > 0n) {
|
|
39166
|
+
const digit = to_number(value % BASE);
|
|
39167
|
+
result = CHARS[digit] + result;
|
|
39168
|
+
value /= BASE;
|
|
39169
|
+
}
|
|
39170
|
+
}
|
|
39171
|
+
if (length) {
|
|
39172
|
+
result = result.padStart(length, PAD);
|
|
39191
39173
|
}
|
|
39192
|
-
return result
|
|
39174
|
+
return result;
|
|
39175
|
+
}
|
|
39176
|
+
function to_number(value) {
|
|
39177
|
+
if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
39178
|
+
return Number(value);
|
|
39179
|
+
}
|
|
39180
|
+
throw new Error("BigInt value outside safe integer range");
|
|
39193
39181
|
}
|
|
39182
|
+
var CHARS = "-0123456789_abcdefghijklmnopqrstuvwxyz".split("").sort().join("");
|
|
39183
|
+
var PAD = CHARS[0];
|
|
39184
|
+
var BASE = BigInt(CHARS.length);
|
|
39185
|
+
var CHAR_BITS = Math.log2(to_number(BASE));
|
|
39186
|
+
var TIMESTAMP_BITS = 53;
|
|
39187
|
+
var RANDOM_BITS = 30;
|
|
39188
|
+
var ID_BITS = TIMESTAMP_BITS + RANDOM_BITS;
|
|
39189
|
+
var ID_LENGTH = Math.ceil(ID_BITS / CHAR_BITS);
|
|
39194
39190
|
|
|
39195
39191
|
// src/commands/Rebase.tsx
|
|
39196
39192
|
function Rebase(props) {
|
|
@@ -39216,7 +39212,7 @@ Rebase.run = async function run5(props) {
|
|
|
39216
39212
|
actions.output(/* @__PURE__ */ React34.createElement(Text, {
|
|
39217
39213
|
color: colors.red
|
|
39218
39214
|
}, "\uD83D\uDEA8 Abort"));
|
|
39219
|
-
|
|
39215
|
+
handle_exit2();
|
|
39220
39216
|
return 19;
|
|
39221
39217
|
});
|
|
39222
39218
|
const temp_branch_name = `${branch_name}_${short_id()}`;
|
|
@@ -39310,7 +39306,7 @@ Rebase.run = async function run5(props) {
|
|
|
39310
39306
|
}
|
|
39311
39307
|
cli.sync(`pwd`, spawn_options);
|
|
39312
39308
|
}
|
|
39313
|
-
function
|
|
39309
|
+
function handle_exit2() {
|
|
39314
39310
|
actions.output(/* @__PURE__ */ React34.createElement(Text, {
|
|
39315
39311
|
color: colors.yellow
|
|
39316
39312
|
}, "Restoring ", /* @__PURE__ */ React34.createElement(Brackets, null, branch_name), "…"));
|
|
@@ -39360,7 +39356,7 @@ async function run6() {
|
|
|
39360
39356
|
actions.output(/* @__PURE__ */ React36.createElement(Text, {
|
|
39361
39357
|
color: colors.red
|
|
39362
39358
|
}, "\uD83D\uDEA8 Abort"));
|
|
39363
|
-
|
|
39359
|
+
handle_exit2();
|
|
39364
39360
|
return 15;
|
|
39365
39361
|
});
|
|
39366
39362
|
const temp_branch_name = `${branch_name}_${short_id()}`;
|
|
@@ -39439,7 +39435,7 @@ async function run6() {
|
|
|
39439
39435
|
}
|
|
39440
39436
|
cli.sync(`pwd`, spawn_options);
|
|
39441
39437
|
}
|
|
39442
|
-
function
|
|
39438
|
+
function handle_exit2() {
|
|
39443
39439
|
actions.output(/* @__PURE__ */ React36.createElement(Text, {
|
|
39444
39440
|
color: colors.yellow
|
|
39445
39441
|
}, "Restoring ", /* @__PURE__ */ React36.createElement(Brackets, null, branch_name), "…"));
|
|
@@ -40098,8 +40094,7 @@ function SelectCommitRangesInternal(props) {
|
|
|
40098
40094
|
let branch_prefix = "";
|
|
40099
40095
|
if (argv["branch-prefix"]) {
|
|
40100
40096
|
branch_prefix = argv["branch-prefix"];
|
|
40101
|
-
} else if (
|
|
40102
|
-
branch_prefix = process.env.GIT_STACK_BRANCH_PREFIX;
|
|
40097
|
+
} else if ("") {
|
|
40103
40098
|
}
|
|
40104
40099
|
return `${branch_prefix}${gs_short_id()}`;
|
|
40105
40100
|
}
|
|
@@ -40258,7 +40253,7 @@ async function run9() {
|
|
|
40258
40253
|
actions.output(/* @__PURE__ */ React44.createElement(Text, {
|
|
40259
40254
|
color: colors.red
|
|
40260
40255
|
}, "\uD83D\uDEA8 Abort"));
|
|
40261
|
-
|
|
40256
|
+
handle_exit2();
|
|
40262
40257
|
return 17;
|
|
40263
40258
|
});
|
|
40264
40259
|
let DEFAULT_PR_BODY = "";
|
|
@@ -40403,7 +40398,7 @@ async function run9() {
|
|
|
40403
40398
|
});
|
|
40404
40399
|
}
|
|
40405
40400
|
}
|
|
40406
|
-
function
|
|
40401
|
+
function handle_exit2() {
|
|
40407
40402
|
actions.output(/* @__PURE__ */ React44.createElement(Text, {
|
|
40408
40403
|
color: colors.yellow
|
|
40409
40404
|
}, "Restoring PR state…"));
|
|
@@ -40709,7 +40704,9 @@ class ErrorBoundary extends React52.Component {
|
|
|
40709
40704
|
component_stack = component_stack.split(`
|
|
40710
40705
|
`).slice(1).join(`
|
|
40711
40706
|
`);
|
|
40712
|
-
this.setState({ component_stack })
|
|
40707
|
+
this.setState({ component_stack }, async () => {
|
|
40708
|
+
await Exit.handle_exit({ code: 30, clear: true });
|
|
40709
|
+
});
|
|
40713
40710
|
}
|
|
40714
40711
|
}
|
|
40715
40712
|
render() {
|
|
@@ -45664,7 +45661,7 @@ var yargs_default = Yargs;
|
|
|
45664
45661
|
|
|
45665
45662
|
// src/command.ts
|
|
45666
45663
|
async function command2() {
|
|
45667
|
-
return yargs_default(hideBin(process.argv)).scriptName("git stack").usage("Usage: git stack [command] [options]").command("$0", "Sync commit ranges to Github", (yargs) => yargs.options(DefaultOptions)).command("fixup [commit]", "Amend staged changes to a specific commit in history", (yargs) => yargs.positional("commit", FixupOptions.commit)).command("log [args...]", "Print an abbreviated log with numbered commits, useful for git stack fixup", (yargs) => yargs.strict(false)).command("rebase", "Update local branch via rebase with latest changes from origin master branch", (yargs) => yargs).option("verbose", GlobalOptions.verbose).wrap(123).strict().version("2.
|
|
45664
|
+
return yargs_default(hideBin(process.argv)).scriptName("git stack").usage("Usage: git stack [command] [options]").command("$0", "Sync commit ranges to Github", (yargs) => yargs.options(DefaultOptions)).command("fixup [commit]", "Amend staged changes to a specific commit in history", (yargs) => yargs.positional("commit", FixupOptions.commit)).command("log [args...]", "Print an abbreviated log with numbered commits, useful for git stack fixup", (yargs) => yargs.strict(false)).command("rebase", "Update local branch via rebase with latest changes from origin master branch", (yargs) => yargs).option("verbose", GlobalOptions.verbose).wrap(123).strict().version("2.4.0").showHidden("show-hidden", "Show hidden options via `git stack help --show-hidden`").help("help", "Show usage via `git stack help`").argv;
|
|
45668
45665
|
}
|
|
45669
45666
|
var GlobalOptions = {
|
|
45670
45667
|
verbose: {
|
package/package.json
CHANGED
package/src/app/Exit.tsx
CHANGED
|
@@ -15,44 +15,44 @@ type Props = {
|
|
|
15
15
|
export function Exit(props: Props) {
|
|
16
16
|
React.useEffect(() => {
|
|
17
17
|
// immediately handle exit on mount
|
|
18
|
-
handle_exit().catch((err) => {
|
|
18
|
+
Exit.handle_exit(props).catch((err) => {
|
|
19
19
|
// eslint-disable-next-line no-console
|
|
20
20
|
console.error(err);
|
|
21
21
|
});
|
|
22
|
+
}, [props.clear, props.code]);
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const actions = state.actions;
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Exit.handle_exit = async function handle_exit(props: Props) {
|
|
28
|
+
const state = Store.getState();
|
|
29
|
+
const actions = state.actions;
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
actions.debug(`[Exit] handle_exit ${JSON.stringify(props)}`);
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
if (state.abort_handler) {
|
|
33
|
-
exit_code = await state.abort_handler();
|
|
34
|
-
}
|
|
33
|
+
let exit_code = props.code;
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
35
|
+
// run abort_handler if it exists
|
|
36
|
+
if (state.abort_handler) {
|
|
37
|
+
exit_code = await state.abort_handler();
|
|
38
|
+
}
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
// restore git stash if necessary
|
|
41
|
+
if (state.is_dirty_check_stash) {
|
|
42
|
+
await cli("git stash pop");
|
|
43
|
+
actions.output(<Ink.Text color={colors.green}>✅ Changes restored from stash</Ink.Text>);
|
|
44
|
+
}
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
actions.clear();
|
|
48
|
-
}
|
|
46
|
+
// ensure output has a chance to render
|
|
47
|
+
await sleep(1);
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
// finally handle the actual app and process exit
|
|
50
|
+
if (props.clear) {
|
|
51
|
+
actions.clear();
|
|
52
|
+
}
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
process.exit();
|
|
54
|
-
}
|
|
55
|
-
}, [props.clear, props.code]);
|
|
54
|
+
actions.unmount();
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
process.exitCode = exit_code;
|
|
57
|
+
process.exit();
|
|
58
|
+
};
|
|
@@ -3,6 +3,7 @@ import * as React from "react";
|
|
|
3
3
|
|
|
4
4
|
import * as Ink from "ink-cjs";
|
|
5
5
|
|
|
6
|
+
import { Exit } from "~/app/Exit";
|
|
6
7
|
import { FormatText } from "~/app/FormatText";
|
|
7
8
|
import { Store } from "~/app/Store";
|
|
8
9
|
import { colors } from "~/core/colors";
|
|
@@ -36,7 +37,9 @@ export class ErrorBoundary extends React.Component<Props, State> {
|
|
|
36
37
|
if (component_stack) {
|
|
37
38
|
// remove first line of component_stack
|
|
38
39
|
component_stack = component_stack.split("\n").slice(1).join("\n");
|
|
39
|
-
this.setState({ component_stack })
|
|
40
|
+
this.setState({ component_stack }, async () => {
|
|
41
|
+
await Exit.handle_exit({ code: 30, clear: true });
|
|
42
|
+
});
|
|
40
43
|
}
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { short_id } from "~/core/short_id";
|
|
4
|
+
|
|
5
|
+
const GENERATE_COUNT = 1000;
|
|
6
|
+
|
|
7
|
+
test("short_id is 15 characters", () => {
|
|
8
|
+
const id = short_id();
|
|
9
|
+
expect(id.length).toBe(16);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("unique enough in practice", () => {
|
|
13
|
+
const id_set = new Set();
|
|
14
|
+
for (let i = 0; i < GENERATE_COUNT; i++) {
|
|
15
|
+
const id = short_id();
|
|
16
|
+
id_set.add(id);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
expect(id_set.size).toBe(GENERATE_COUNT);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("sorts lexicographically", () => {
|
|
23
|
+
const id_list = [];
|
|
24
|
+
for (let i = 0; i < GENERATE_COUNT; i++) {
|
|
25
|
+
const id = short_id();
|
|
26
|
+
id_list.push(id);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
expect(id_list.sort()).toEqual(id_list);
|
|
30
|
+
});
|
package/src/core/short_id.ts
CHANGED
|
@@ -1,87 +1,70 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
|
|
3
3
|
export function short_id() {
|
|
4
|
-
|
|
4
|
+
// wall-clock milliseconds
|
|
5
|
+
const timestamp = BigInt(Date.now());
|
|
5
6
|
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
// (2^53) * (2^10) = 2^63 = 9,223,372,036,854,775,808
|
|
9
|
-
const js_max_bits = 53;
|
|
7
|
+
// random int value between 0 and 2^RANDOM_BITS - 1
|
|
8
|
+
const random = BigInt(crypto.randomInt(0, 1 << RANDOM_BITS));
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
// concatenate timestamp (high bits) and random value
|
|
11
|
+
const combined = (timestamp << BigInt(RANDOM_BITS)) | random;
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
const padding_bits = js_max_bits - timestamp_bits;
|
|
13
|
+
const id = encode(combined, ID_LENGTH);
|
|
15
14
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// combine timestamp and random value
|
|
20
|
-
const combined = interleave_bits(timestamp, random);
|
|
21
|
-
|
|
22
|
-
// console.debug({ combined, timestamp, random, padding_bits, timestamp_bits });
|
|
23
|
-
|
|
24
|
-
return encode(combined);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function binary(value: number) {
|
|
28
|
-
return BigInt(value).toString(2);
|
|
15
|
+
// console.debug({ id, timestamp, random, combined });
|
|
16
|
+
return id;
|
|
29
17
|
}
|
|
30
18
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
function interleave_bits(a: number, b: number) {
|
|
36
|
-
const a_binary = binary(a).split("");
|
|
37
|
-
|
|
38
|
-
const b_binary = binary(b).split("");
|
|
39
|
-
|
|
40
|
-
while (b_binary.length) {
|
|
41
|
-
// pull random bit out of b_binary
|
|
42
|
-
|
|
43
|
-
const b_index = rand_index(b_binary);
|
|
44
|
-
|
|
45
|
-
const [selected] = b_binary.splice(b_index, 1);
|
|
46
|
-
|
|
47
|
-
// insert random bit into a_binary
|
|
19
|
+
// converting value into base based on available chars
|
|
20
|
+
function encode(value: bigint, length?: number) {
|
|
21
|
+
let result = "";
|
|
48
22
|
|
|
49
|
-
|
|
23
|
+
// avoid zero division
|
|
24
|
+
if (value > 0n) {
|
|
25
|
+
while (value > 0n) {
|
|
26
|
+
// convert least significant digit
|
|
27
|
+
const digit = to_number(value % BASE);
|
|
28
|
+
result = CHARS[digit] + result;
|
|
50
29
|
|
|
51
|
-
|
|
30
|
+
// drop the digit we just divided by
|
|
31
|
+
value /= BASE;
|
|
32
|
+
}
|
|
52
33
|
}
|
|
53
34
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
35
|
+
// left pad with 0 to guarantee chars
|
|
36
|
+
if (length) {
|
|
37
|
+
result = result.padStart(length, PAD);
|
|
38
|
+
}
|
|
57
39
|
|
|
58
|
-
return
|
|
40
|
+
return result;
|
|
59
41
|
}
|
|
60
42
|
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// of bits in value divided by bits per character in encoding
|
|
65
|
-
//
|
|
66
|
-
// Example
|
|
67
|
-
// in base64 each characters can represent 6 bits (2^6 = 64)
|
|
68
|
-
// 53 bits / 6 bits = 8.833333333333334 characters (9 characters)
|
|
69
|
-
//
|
|
70
|
-
// prettier-ignore
|
|
71
|
-
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-+";
|
|
72
|
-
|
|
73
|
-
const bits_per_char = Math.log2(chars.length);
|
|
74
|
-
const max_value_bits = 53;
|
|
75
|
-
const max_char_size = Math.ceil(max_value_bits / bits_per_char);
|
|
76
|
-
|
|
77
|
-
let result = "";
|
|
78
|
-
|
|
79
|
-
while (value > 0) {
|
|
80
|
-
result = chars[value % chars.length] + result;
|
|
81
|
-
|
|
82
|
-
value = Math.floor(value / chars.length);
|
|
43
|
+
function to_number(value: bigint) {
|
|
44
|
+
if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
45
|
+
return Number(value);
|
|
83
46
|
}
|
|
84
47
|
|
|
85
|
-
|
|
86
|
-
return result.padStart(max_char_size, "=");
|
|
48
|
+
throw new Error("BigInt value outside safe integer range");
|
|
87
49
|
}
|
|
50
|
+
|
|
51
|
+
// valid characters to use for the short id
|
|
52
|
+
// use only lowercase letters and numbers to avoid
|
|
53
|
+
// confusing file system issues with git branch names
|
|
54
|
+
const CHARS = "-0123456789_abcdefghijklmnopqrstuvwxyz".split("").sort().join("");
|
|
55
|
+
const PAD = CHARS[0];
|
|
56
|
+
const BASE = BigInt(CHARS.length);
|
|
57
|
+
// bits carried by each char log2(BASE) ≈ 5.32
|
|
58
|
+
const CHAR_BITS = Math.log2(to_number(BASE));
|
|
59
|
+
|
|
60
|
+
// javascript Date.now returns a Number that will overflow eventually
|
|
61
|
+
// 2^53 max integer, overflows in 2^53 milliseconds (285_616 years)
|
|
62
|
+
// (1n<<53n) / 1_000n / 60n / 60n / 24n / 365n ≈ 285616n
|
|
63
|
+
const TIMESTAMP_BITS = 53;
|
|
64
|
+
|
|
65
|
+
// collision probability for identical timestamps (milliseconds)
|
|
66
|
+
// ≈ 1 / 2^30 ≈ 1 / 1_073_741_824 (1.1 billion)
|
|
67
|
+
const RANDOM_BITS = 30;
|
|
68
|
+
|
|
69
|
+
const ID_BITS = TIMESTAMP_BITS + RANDOM_BITS;
|
|
70
|
+
const ID_LENGTH = Math.ceil(ID_BITS / CHAR_BITS);
|