This commit is contained in:
j3d1 2026-08-21 21:37:12 +02:00
parent 7d9f67a77a
commit 3299c97392
13 changed files with 1407 additions and 327 deletions

View file

@ -0,0 +1,143 @@
<template>
<BaseLayout hide-search>
<main class="content">
<div class="container-fluid p-0">
<div class="card">
<div class="card-header">Decoded</div>
<div class="card-body">
<ul>
<li v-for="(value, index) in decoded" :key="index">{{ value }}</li>
</ul>
</div>
</div>
<div class="card">
<div class="card-header">Deserialized</div>
<div class="card-body">
<p>kind: {{ deserialized.kind }}</p>
<ul>
<li v-for="(value, name) in fields" :key="name">{{ name }}: {{ value }}</li>
</ul>
</div>
</div>
<div class="card">
<div class="card-header">Examples</div>
<table class="table table-striped mb-0">
<thead>
<tr>
<th>Kind</th>
<th>Fields</th>
<th>Serialized</th>
<th>Bits</th>
<th>Token</th>
</tr>
</thead>
<tbody>
<tr v-for="example in examples" :key="example.token">
<td>{{ example.kind }}</td>
<td>
<span v-for="(value, name) in example.fields" :key="name" class="me-2">
{{ name }}: {{ value }}
</span>
</td>
<td>[{{ example.ints.join(', ') }}]</td>
<td><code>{{ example.bits }}</code></td>
<td>
<router-link :to="'/' + example.token">{{ example.token }}</router-link>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</BaseLayout>
</template>
<script>
import BaseLayout from '@/components/BaseLayout.vue';
import {decodeShortId, deserializeShortId, encodeShortId, serializeShortId} from '@/short-id';
const EXAMPLES = [
{kind: 'item', owner_identity_id: 7, item_local_id: 42},
{kind: 'storage_location', owner_identity_id: 3, storage_location_id: 1000},
{kind: 'category', category_id: 5},
{kind: 'workflow', owner_identity_id: 2, workflow_id: 9},
{kind: 'group', group_id: 11},
{kind: 'file', file_id: 123},
];
// Debug-only display helper, kept out of short-id.js's production library: mirrors its bit-packing
// rules (2-bit kind tag with an all-ones escape, 4-bit continuation chunks, see
// docs/handles-and-shortids.md) just to show them, recomputed straight from an already-serialized
// `ints` list rather than re-parsing a token.
const KIND_TAG_BITS = 2;
const CHUNK_BITS = 4;
const DIRECT_KIND_COUNT = 2 ** KIND_TAG_BITS - 1;
function chunkedIntBits(value, chunkBits) {
value = BigInt(value);
const mask = (1n << BigInt(chunkBits)) - 1n;
const chunks = [];
let remaining = value;
do {
chunks.unshift(remaining & mask);
remaining >>= BigInt(chunkBits);
} while (remaining > 0n);
return chunks.map((chunk, i) => {
const continuation = i === chunks.length - 1 ? '0' : '1';
return continuation + chunk.toString(2).padStart(chunkBits, '0');
}).join('');
}
function shortIdBitSegments(ints) {
const [kindIndex, ...fieldValues] = ints;
const segments = [];
if (kindIndex < DIRECT_KIND_COUNT) {
segments.push(kindIndex.toString(2).padStart(KIND_TAG_BITS, '0'));
} else {
segments.push('1'.repeat(KIND_TAG_BITS));
segments.push(chunkedIntBits(kindIndex - DIRECT_KIND_COUNT, CHUNK_BITS));
}
fieldValues.forEach(value => segments.push(chunkedIntBits(value, CHUNK_BITS)));
const totalBits = segments.join('').length;
const paddingBits = (6 - (totalBits % 6)) % 6;
if (paddingBits > 0) {
segments.push('0'.repeat(paddingBits));
}
return segments.join(' ');
}
export default {
name: 'ShortId',
components: {
BaseLayout
},
props: {
short_id: {
type: String,
required: true
}
},
computed: {
decoded() {
return decodeShortId(this.short_id);
},
deserialized() {
return deserializeShortId(this.decoded);
},
fields() {
const {kind, ...fields} = this.deserialized;
return fields;
},
examples() {
return EXAMPLES.map(named => {
const {kind, ...fields} = named;
const ints = serializeShortId(named);
const token = encodeShortId(ints);
return {kind, fields, ints, token, bits: shortIdBitSegments(ints)};
});
}
}
}
</script>