@syncular/core 0.0.4-25 → 0.0.4-32

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,14 @@
1
+ /**
2
+ * Gzip-compress a byte array using CompressionStream when available,
3
+ * with node:zlib fallback for Node/Bun runtimes.
4
+ */
5
+ export declare function gzipBytes(payload: Uint8Array): Promise<Uint8Array>;
6
+ /**
7
+ * Gzip-compress bytes and return a stream. When streaming compression is not
8
+ * available, falls back to eager compression and includes byteLength metadata.
9
+ */
10
+ export declare function gzipBytesToStream(payload: Uint8Array): Promise<{
11
+ stream: ReadableStream<Uint8Array>;
12
+ byteLength?: number;
13
+ }>;
14
+ //# sourceMappingURL=compression.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compression.d.ts","sourceRoot":"","sources":["../../src/utils/compression.ts"],"names":[],"mappings":"AAwCA;;;GAGG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAqBxE;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC;IACpE,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC,CAgBD"}
@@ -0,0 +1,77 @@
1
+ function bytesToReadableStream(bytes) {
2
+ return new ReadableStream({
3
+ start(controller) {
4
+ controller.enqueue(bytes);
5
+ controller.close();
6
+ },
7
+ });
8
+ }
9
+ async function streamToBytes(stream) {
10
+ const reader = stream.getReader();
11
+ const chunks = [];
12
+ let total = 0;
13
+ try {
14
+ while (true) {
15
+ const { done, value } = await reader.read();
16
+ if (done)
17
+ break;
18
+ if (!value)
19
+ continue;
20
+ chunks.push(value);
21
+ total += value.length;
22
+ }
23
+ }
24
+ finally {
25
+ reader.releaseLock();
26
+ }
27
+ if (chunks.length === 0)
28
+ return new Uint8Array();
29
+ if (chunks.length === 1)
30
+ return chunks[0] ?? new Uint8Array();
31
+ const merged = new Uint8Array(total);
32
+ let offset = 0;
33
+ for (const chunk of chunks) {
34
+ merged.set(chunk, offset);
35
+ offset += chunk.length;
36
+ }
37
+ return merged;
38
+ }
39
+ /**
40
+ * Gzip-compress a byte array using CompressionStream when available,
41
+ * with node:zlib fallback for Node/Bun runtimes.
42
+ */
43
+ export async function gzipBytes(payload) {
44
+ if (typeof CompressionStream !== 'undefined') {
45
+ const stream = bytesToReadableStream(payload).pipeThrough(new CompressionStream('gzip'));
46
+ return streamToBytes(stream);
47
+ }
48
+ const nodeZlib = await import('node:zlib');
49
+ return await new Promise((resolve, reject) => {
50
+ nodeZlib.gzip(payload, (error, compressed) => {
51
+ if (error) {
52
+ reject(error);
53
+ return;
54
+ }
55
+ resolve(new Uint8Array(compressed));
56
+ });
57
+ });
58
+ }
59
+ /**
60
+ * Gzip-compress bytes and return a stream. When streaming compression is not
61
+ * available, falls back to eager compression and includes byteLength metadata.
62
+ */
63
+ export async function gzipBytesToStream(payload) {
64
+ if (typeof CompressionStream !== 'undefined') {
65
+ const source = bytesToReadableStream(payload);
66
+ const gzipStream = new CompressionStream('gzip');
67
+ return {
68
+ stream: source.pipeThrough(gzipStream),
69
+ };
70
+ }
71
+ const compressed = await gzipBytes(payload);
72
+ return {
73
+ stream: bytesToReadableStream(compressed),
74
+ byteLength: compressed.length,
75
+ };
76
+ }
77
+ //# sourceMappingURL=compression.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compression.js","sourceRoot":"","sources":["../../src/utils/compression.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Cross-runtime SHA-256 digest helper.
3
+ *
4
+ * Uses Web Crypto when available, with Node crypto fallback.
5
+ */
6
+ export declare function sha256Hex(input: string | Uint8Array): Promise<string>;
7
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/utils/crypto.ts"],"names":[],"mappings":"AAQA;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAmB3E"}
@@ -0,0 +1,25 @@
1
+ const textEncoder = new TextEncoder();
2
+ function toHex(bytes) {
3
+ return Array.from(bytes)
4
+ .map((byte) => byte.toString(16).padStart(2, '0'))
5
+ .join('');
6
+ }
7
+ /**
8
+ * Cross-runtime SHA-256 digest helper.
9
+ *
10
+ * Uses Web Crypto when available, with Node crypto fallback.
11
+ */
12
+ export async function sha256Hex(input) {
13
+ const payload = typeof input === 'string' ? textEncoder.encode(input) : input;
14
+ if (typeof crypto !== 'undefined' && crypto.subtle) {
15
+ const digestBuffer = await crypto.subtle.digest('SHA-256', payload.slice().buffer);
16
+ return toHex(new Uint8Array(digestBuffer));
17
+ }
18
+ try {
19
+ const nodeCrypto = await import('node:crypto');
20
+ return nodeCrypto.createHash('sha256').update(payload).digest('hex');
21
+ }
22
+ catch { }
23
+ throw new Error('Failed to create SHA-256 hash, no crypto implementation available');
24
+ }
25
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/utils/crypto.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,GAAG,IAAI,WAAW,EAAE,CAAC;AAEtC,SAAS,KAAK,CAAC,KAAiB,EAAU;IACxC,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;SACrB,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;SACjD,IAAI,CAAC,EAAE,CAAC,CAAC;AAAA,CACb;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,KAA0B,EAAmB;IAC3E,MAAM,OAAO,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAE9E,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QACnD,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAC7C,SAAS,EACT,OAAO,CAAC,KAAK,EAAE,CAAC,MAAM,CACvB,CAAC;QACF,OAAO,KAAK,CAAC,IAAI,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC;IAC7C,CAAC;IAED,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;QAC/C,OAAO,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IAEV,MAAM,IAAI,KAAK,CACb,mEAAmE,CACpE,CAAC;AAAA,CACH"}
@@ -1,2 +1,3 @@
1
+ export declare function generateId(): string;
1
2
  export declare function randomId(): string;
2
3
  //# sourceMappingURL=id.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/utils/id.ts"],"names":[],"mappings":"AAAA,wBAAgB,QAAQ,IAAI,MAAM,CAMjC"}
1
+ {"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/utils/id.ts"],"names":[],"mappings":"AAAA,wBAAgB,UAAU,IAAI,MAAM,CAmBnC;AAED,wBAAgB,QAAQ,IAAI,MAAM,CAEjC"}
package/dist/utils/id.js CHANGED
@@ -1,8 +1,22 @@
1
- export function randomId() {
2
- const cryptoObj = globalThis.crypto;
1
+ export function generateId() {
2
+ const cryptoObj = typeof crypto !== 'undefined' ? crypto : globalThis.crypto;
3
3
  if (cryptoObj && typeof cryptoObj.randomUUID === 'function') {
4
4
  return cryptoObj.randomUUID();
5
5
  }
6
+ if (cryptoObj && typeof cryptoObj.getRandomValues === 'function') {
7
+ const bytes = new Uint8Array(16);
8
+ cryptoObj.getRandomValues(bytes);
9
+ // UUID v4 variant bits.
10
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
11
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
12
+ const hex = Array.from(bytes)
13
+ .map((byte) => byte.toString(16).padStart(2, '0'))
14
+ .join('');
15
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
16
+ }
6
17
  return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
7
18
  }
19
+ export function randomId() {
20
+ return generateId();
21
+ }
8
22
  //# sourceMappingURL=id.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"id.js","sourceRoot":"","sources":["../../src/utils/id.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,QAAQ,GAAW;IACjC,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC;IACpC,IAAI,SAAS,IAAI,OAAO,SAAS,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;QAC5D,OAAO,SAAS,CAAC,UAAU,EAAE,CAAC;IAChC,CAAC;IACD,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAAA,CAC/D"}
1
+ {"version":3,"file":"id.js","sourceRoot":"","sources":["../../src/utils/id.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,UAAU,GAAW;IACnC,MAAM,SAAS,GAAG,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC;IAC7E,IAAI,SAAS,IAAI,OAAO,SAAS,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;QAC5D,OAAO,SAAS,CAAC,UAAU,EAAE,CAAC;IAChC,CAAC;IAED,IAAI,SAAS,IAAI,OAAO,SAAS,CAAC,eAAe,KAAK,UAAU,EAAE,CAAC;QACjE,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;QACjC,SAAS,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;QACjC,wBAAwB;QACxB,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;QACrC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;QACrC,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;aAC1B,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;aACjD,IAAI,CAAC,EAAE,CAAC,CAAC;QACZ,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;IACjH,CAAC;IAED,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAAA,CAC/D;AAED,MAAM,UAAU,QAAQ,GAAW;IACjC,OAAO,UAAU,EAAE,CAAC;AAAA,CACrB"}
@@ -1,3 +1,5 @@
1
+ export * from './compression';
2
+ export * from './crypto';
1
3
  export * from './id';
2
4
  export * from './object';
3
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,MAAM,CAAC;AACrB,cAAc,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,MAAM,CAAC;AACrB,cAAc,UAAU,CAAC"}
@@ -1,3 +1,5 @@
1
+ export * from './compression.js';
2
+ export * from './crypto.js';
1
3
  export * from './id.js';
2
4
  export * from './object.js';
3
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,MAAM,CAAC;AACrB,cAAc,UAAU,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,MAAM,CAAC;AACrB,cAAc,UAAU,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/core",
3
- "version": "0.0.4-25",
3
+ "version": "0.0.4-32",
4
4
  "description": "Core protocol types and shared utilities for the Syncular sync framework",
5
5
  "license": "MIT",
6
6
  "author": "Benjamin Kniffler",
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { gunzipSync } from 'node:zlib';
3
+ import { gzipBytes, gzipBytesToStream } from '../utils';
4
+
5
+ async function readStream(
6
+ stream: ReadableStream<Uint8Array>
7
+ ): Promise<Uint8Array> {
8
+ const reader = stream.getReader();
9
+ const chunks: Uint8Array[] = [];
10
+ let total = 0;
11
+
12
+ while (true) {
13
+ const { done, value } = await reader.read();
14
+ if (done) break;
15
+ if (!value) continue;
16
+ chunks.push(value);
17
+ total += value.length;
18
+ }
19
+
20
+ if (chunks.length === 0) return new Uint8Array();
21
+ if (chunks.length === 1) return chunks[0] ?? new Uint8Array();
22
+
23
+ const merged = new Uint8Array(total);
24
+ let offset = 0;
25
+ for (const chunk of chunks) {
26
+ merged.set(chunk, offset);
27
+ offset += chunk.length;
28
+ }
29
+ return merged;
30
+ }
31
+
32
+ describe('gzipBytes', () => {
33
+ it('compresses bytes that can be decompressed via gunzip', async () => {
34
+ const payload = new TextEncoder().encode(
35
+ 'syncular compression test '.repeat(64)
36
+ );
37
+ const compressed = await gzipBytes(payload);
38
+ const decompressed = new Uint8Array(gunzipSync(compressed));
39
+ expect(decompressed).toEqual(payload);
40
+ });
41
+ });
42
+
43
+ describe('gzipBytesToStream', () => {
44
+ it('returns a gzip stream that round-trips', async () => {
45
+ const payload = new TextEncoder().encode('stream compression '.repeat(64));
46
+ const result = await gzipBytesToStream(payload);
47
+ const compressed = await readStream(result.stream);
48
+ const decompressed = new Uint8Array(gunzipSync(compressed));
49
+ expect(decompressed).toEqual(payload);
50
+ if (typeof result.byteLength === 'number') {
51
+ expect(result.byteLength).toBe(compressed.length);
52
+ }
53
+ });
54
+ });
@@ -0,0 +1,91 @@
1
+ function bytesToReadableStream(bytes: Uint8Array): ReadableStream<Uint8Array> {
2
+ return new ReadableStream<Uint8Array>({
3
+ start(controller) {
4
+ controller.enqueue(bytes);
5
+ controller.close();
6
+ },
7
+ });
8
+ }
9
+
10
+ async function streamToBytes(
11
+ stream: ReadableStream<Uint8Array>
12
+ ): Promise<Uint8Array> {
13
+ const reader = stream.getReader();
14
+ const chunks: Uint8Array[] = [];
15
+ let total = 0;
16
+
17
+ try {
18
+ while (true) {
19
+ const { done, value } = await reader.read();
20
+ if (done) break;
21
+ if (!value) continue;
22
+ chunks.push(value);
23
+ total += value.length;
24
+ }
25
+ } finally {
26
+ reader.releaseLock();
27
+ }
28
+
29
+ if (chunks.length === 0) return new Uint8Array();
30
+ if (chunks.length === 1) return chunks[0] ?? new Uint8Array();
31
+
32
+ const merged = new Uint8Array(total);
33
+ let offset = 0;
34
+ for (const chunk of chunks) {
35
+ merged.set(chunk, offset);
36
+ offset += chunk.length;
37
+ }
38
+ return merged;
39
+ }
40
+
41
+ /**
42
+ * Gzip-compress a byte array using CompressionStream when available,
43
+ * with node:zlib fallback for Node/Bun runtimes.
44
+ */
45
+ export async function gzipBytes(payload: Uint8Array): Promise<Uint8Array> {
46
+ if (typeof CompressionStream !== 'undefined') {
47
+ const stream = bytesToReadableStream(payload).pipeThrough(
48
+ new CompressionStream('gzip') as unknown as TransformStream<
49
+ Uint8Array,
50
+ Uint8Array
51
+ >
52
+ );
53
+ return streamToBytes(stream);
54
+ }
55
+
56
+ const nodeZlib = await import('node:zlib');
57
+ return await new Promise<Uint8Array>((resolve, reject) => {
58
+ nodeZlib.gzip(payload, (error, compressed) => {
59
+ if (error) {
60
+ reject(error);
61
+ return;
62
+ }
63
+ resolve(new Uint8Array(compressed));
64
+ });
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Gzip-compress bytes and return a stream. When streaming compression is not
70
+ * available, falls back to eager compression and includes byteLength metadata.
71
+ */
72
+ export async function gzipBytesToStream(payload: Uint8Array): Promise<{
73
+ stream: ReadableStream<Uint8Array>;
74
+ byteLength?: number;
75
+ }> {
76
+ if (typeof CompressionStream !== 'undefined') {
77
+ const source = bytesToReadableStream(payload);
78
+ const gzipStream = new CompressionStream(
79
+ 'gzip'
80
+ ) as unknown as TransformStream<Uint8Array, Uint8Array>;
81
+ return {
82
+ stream: source.pipeThrough(gzipStream),
83
+ };
84
+ }
85
+
86
+ const compressed = await gzipBytes(payload);
87
+ return {
88
+ stream: bytesToReadableStream(compressed),
89
+ byteLength: compressed.length,
90
+ };
91
+ }
@@ -0,0 +1,33 @@
1
+ const textEncoder = new TextEncoder();
2
+
3
+ function toHex(bytes: Uint8Array): string {
4
+ return Array.from(bytes)
5
+ .map((byte) => byte.toString(16).padStart(2, '0'))
6
+ .join('');
7
+ }
8
+
9
+ /**
10
+ * Cross-runtime SHA-256 digest helper.
11
+ *
12
+ * Uses Web Crypto when available, with Node crypto fallback.
13
+ */
14
+ export async function sha256Hex(input: string | Uint8Array): Promise<string> {
15
+ const payload = typeof input === 'string' ? textEncoder.encode(input) : input;
16
+
17
+ if (typeof crypto !== 'undefined' && crypto.subtle) {
18
+ const digestBuffer = await crypto.subtle.digest(
19
+ 'SHA-256',
20
+ payload.slice().buffer
21
+ );
22
+ return toHex(new Uint8Array(digestBuffer));
23
+ }
24
+
25
+ try {
26
+ const nodeCrypto = await import('node:crypto');
27
+ return nodeCrypto.createHash('sha256').update(payload).digest('hex');
28
+ } catch {}
29
+
30
+ throw new Error(
31
+ 'Failed to create SHA-256 hash, no crypto implementation available'
32
+ );
33
+ }
package/src/utils/id.ts CHANGED
@@ -1,7 +1,24 @@
1
- export function randomId(): string {
2
- const cryptoObj = globalThis.crypto;
1
+ export function generateId(): string {
2
+ const cryptoObj = typeof crypto !== 'undefined' ? crypto : globalThis.crypto;
3
3
  if (cryptoObj && typeof cryptoObj.randomUUID === 'function') {
4
4
  return cryptoObj.randomUUID();
5
5
  }
6
+
7
+ if (cryptoObj && typeof cryptoObj.getRandomValues === 'function') {
8
+ const bytes = new Uint8Array(16);
9
+ cryptoObj.getRandomValues(bytes);
10
+ // UUID v4 variant bits.
11
+ bytes[6] = (bytes[6]! & 0x0f) | 0x40;
12
+ bytes[8] = (bytes[8]! & 0x3f) | 0x80;
13
+ const hex = Array.from(bytes)
14
+ .map((byte) => byte.toString(16).padStart(2, '0'))
15
+ .join('');
16
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
17
+ }
18
+
6
19
  return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
7
20
  }
21
+
22
+ export function randomId(): string {
23
+ return generateId();
24
+ }
@@ -1,2 +1,4 @@
1
+ export * from './compression';
2
+ export * from './crypto';
1
3
  export * from './id';
2
4
  export * from './object';