@nexcraft-dev/r2d1 0.0.0-stage → 0.1.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/LICENSE +201 -0
- package/README.md +165 -2
- package/dist/core/codec.d.ts +7 -0
- package/dist/core/codec.js +16 -0
- package/dist/core/codec.js.map +1 -0
- package/dist/core/collection.d.ts +32 -0
- package/dist/core/collection.js +193 -0
- package/dist/core/collection.js.map +1 -0
- package/dist/core/cursor.d.ts +5 -0
- package/dist/core/cursor.js +139 -0
- package/dist/core/cursor.js.map +1 -0
- package/dist/core/errors.d.ts +27 -0
- package/dist/core/errors.js +42 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/model.d.ts +91 -0
- package/dist/core/model.js +54 -0
- package/dist/core/model.js.map +1 -0
- package/dist/core/query.d.ts +29 -0
- package/dist/core/query.js +65 -0
- package/dist/core/query.js.map +1 -0
- package/dist/core/schema.d.ts +21 -0
- package/dist/core/schema.js +61 -0
- package/dist/core/schema.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/node/d1.d.ts +10 -0
- package/dist/node/d1.js +57 -0
- package/dist/node/d1.js.map +1 -0
- package/dist/node/index.d.ts +4 -0
- package/dist/node/index.js +3 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/r2.d.ts +23 -0
- package/dist/node/r2.js +115 -0
- package/dist/node/r2.js.map +1 -0
- package/dist/storage/d1.d.ts +28 -0
- package/dist/storage/d1.js +301 -0
- package/dist/storage/d1.js.map +1 -0
- package/dist/storage/r2-key.d.ts +4 -0
- package/dist/storage/r2-key.js +36 -0
- package/dist/storage/r2-key.js.map +1 -0
- package/dist/workers/index.d.ts +47 -0
- package/dist/workers/index.js +93 -0
- package/dist/workers/index.js.map +1 -0
- package/examples/common.ts +54 -0
- package/examples/node.ts +36 -0
- package/examples/workers.ts +22 -0
- package/package.json +61 -4
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IndexCursor } from './model.js';
|
|
2
|
+
/** Java UTF-8 replaces unpaired UTF-16 surrogates with '?'. */
|
|
3
|
+
export declare function javaUtf8(value: string): Uint8Array;
|
|
4
|
+
export declare function encodeCursor(cursor: IndexCursor): string;
|
|
5
|
+
export declare function decodeCursor(cursor: string): IndexCursor;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { key, long } from './model.js';
|
|
2
|
+
/** Java UTF-8 replaces unpaired UTF-16 surrogates with '?'. */
|
|
3
|
+
export function javaUtf8(value) {
|
|
4
|
+
return new TextEncoder().encode(value.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDFFF]/g, (part) => part.length === 2 ? part : '?'));
|
|
5
|
+
}
|
|
6
|
+
function base64(bytes) {
|
|
7
|
+
let binary = '';
|
|
8
|
+
for (let offset = 0; offset < bytes.length; offset += 8192)
|
|
9
|
+
binary += String.fromCharCode(...bytes.subarray(offset, offset + 8192));
|
|
10
|
+
return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
|
11
|
+
}
|
|
12
|
+
export function encodeCursor(cursor) {
|
|
13
|
+
const documentKey = key(cursor.lastDocumentKey.collection, cursor.lastDocumentKey.id);
|
|
14
|
+
const collection = javaUtf8(documentKey.collection);
|
|
15
|
+
const id = javaUtf8(documentKey.id);
|
|
16
|
+
const sort = cursor.sortValue;
|
|
17
|
+
let payload = new Uint8Array();
|
|
18
|
+
let tag = 0;
|
|
19
|
+
if (sort) {
|
|
20
|
+
switch (sort.type) {
|
|
21
|
+
case 'string': {
|
|
22
|
+
tag = 1;
|
|
23
|
+
const text = javaUtf8(sort.value);
|
|
24
|
+
payload = new Uint8Array(4 + text.length);
|
|
25
|
+
new DataView(payload.buffer).setInt32(0, text.length);
|
|
26
|
+
payload.set(text, 4);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case 'long':
|
|
30
|
+
tag = 2;
|
|
31
|
+
payload = new Uint8Array(8);
|
|
32
|
+
new DataView(payload.buffer).setBigInt64(0, long(sort.value));
|
|
33
|
+
break;
|
|
34
|
+
case 'double':
|
|
35
|
+
tag = 3;
|
|
36
|
+
payload = new Uint8Array(8);
|
|
37
|
+
new DataView(payload.buffer).setFloat64(0, sort.value);
|
|
38
|
+
break;
|
|
39
|
+
case 'boolean':
|
|
40
|
+
tag = 4;
|
|
41
|
+
payload = new Uint8Array([sort.value ? 1 : 0]);
|
|
42
|
+
break;
|
|
43
|
+
case 'timestamp':
|
|
44
|
+
if (sort.seconds < -31557014167219200n ||
|
|
45
|
+
sort.seconds > 31556889864403199n ||
|
|
46
|
+
!Number.isInteger(sort.nanos) ||
|
|
47
|
+
sort.nanos < 0 ||
|
|
48
|
+
sort.nanos > 999999999)
|
|
49
|
+
throw new TypeError('invalid timestamp');
|
|
50
|
+
tag = 5;
|
|
51
|
+
payload = new Uint8Array(12);
|
|
52
|
+
new DataView(payload.buffer).setBigInt64(0, sort.seconds);
|
|
53
|
+
new DataView(payload.buffer).setInt32(8, sort.nanos);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const bytes = new Uint8Array(10 + collection.length + id.length + payload.length);
|
|
58
|
+
const view = new DataView(bytes.buffer);
|
|
59
|
+
bytes[0] = 1;
|
|
60
|
+
view.setInt32(1, collection.length);
|
|
61
|
+
bytes.set(collection, 5);
|
|
62
|
+
view.setInt32(5 + collection.length, id.length);
|
|
63
|
+
bytes.set(id, 9 + collection.length);
|
|
64
|
+
bytes[9 + collection.length + id.length] = tag;
|
|
65
|
+
bytes.set(payload, 10 + collection.length + id.length);
|
|
66
|
+
return base64(bytes);
|
|
67
|
+
}
|
|
68
|
+
export function decodeCursor(cursor) {
|
|
69
|
+
try {
|
|
70
|
+
if (typeof cursor !== 'string' || !/^[A-Za-z0-9_-]+={0,2}$/.test(cursor))
|
|
71
|
+
throw new TypeError();
|
|
72
|
+
const binary = atob(cursor.replaceAll('-', '+').replaceAll('_', '/'));
|
|
73
|
+
const bytes = Uint8Array.from(binary, (ch) => ch.charCodeAt(0));
|
|
74
|
+
const view = new DataView(bytes.buffer);
|
|
75
|
+
let offset = 0;
|
|
76
|
+
const byte = () => view.getUint8(offset++);
|
|
77
|
+
const string = () => {
|
|
78
|
+
const length = view.getInt32(offset);
|
|
79
|
+
offset += 4;
|
|
80
|
+
if (length < 0 || length > bytes.length - offset)
|
|
81
|
+
throw new TypeError();
|
|
82
|
+
const value = new TextDecoder('utf-8', { ignoreBOM: true }).decode(bytes.subarray(offset, offset + length));
|
|
83
|
+
offset += length;
|
|
84
|
+
return value;
|
|
85
|
+
};
|
|
86
|
+
if (byte() !== 1)
|
|
87
|
+
throw new TypeError();
|
|
88
|
+
const lastDocumentKey = key(string(), string());
|
|
89
|
+
let sortValue;
|
|
90
|
+
switch (byte()) {
|
|
91
|
+
case 0:
|
|
92
|
+
break;
|
|
93
|
+
case 1:
|
|
94
|
+
sortValue = { type: 'string', value: string() };
|
|
95
|
+
break;
|
|
96
|
+
case 2:
|
|
97
|
+
sortValue = { type: 'long', value: view.getBigInt64(offset) };
|
|
98
|
+
offset += 8;
|
|
99
|
+
break;
|
|
100
|
+
case 3:
|
|
101
|
+
sortValue = { type: 'double', value: view.getFloat64(offset) };
|
|
102
|
+
offset += 8;
|
|
103
|
+
break;
|
|
104
|
+
case 4: {
|
|
105
|
+
const value = byte();
|
|
106
|
+
if (value > 1)
|
|
107
|
+
throw new TypeError();
|
|
108
|
+
sortValue = { type: 'boolean', value: value === 1 };
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case 5: {
|
|
112
|
+
let seconds = view.getBigInt64(offset);
|
|
113
|
+
offset += 8;
|
|
114
|
+
let nanos = view.getInt32(offset);
|
|
115
|
+
offset += 4;
|
|
116
|
+
// Instant.ofEpochSecond normalizes nanosecond adjustments.
|
|
117
|
+
const carry = Math.floor(nanos / 1_000_000_000);
|
|
118
|
+
seconds += BigInt(carry);
|
|
119
|
+
nanos -= carry * 1_000_000_000;
|
|
120
|
+
if (seconds < -31557014167219200n || seconds > 31556889864403199n)
|
|
121
|
+
throw new TypeError();
|
|
122
|
+
sortValue = { type: 'timestamp', seconds, nanos };
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
default:
|
|
126
|
+
throw new TypeError();
|
|
127
|
+
}
|
|
128
|
+
if (offset !== bytes.length)
|
|
129
|
+
throw new TypeError();
|
|
130
|
+
return Object.freeze({
|
|
131
|
+
lastDocumentKey,
|
|
132
|
+
...(sortValue ? { sortValue: Object.freeze(sortValue) } : {}),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (cause) {
|
|
136
|
+
throw new TypeError('invalid cursor', { cause });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/core/cursor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAGvC,+DAA+D;AAC/D,MAAM,UAAU,QAAQ,CAAC,KAAa;IACpC,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAC7B,KAAK,CAAC,OAAO,CAAC,iDAAiD,EAAE,CAAC,IAAI,EAAE,EAAE,CACxE,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAC/B,CACF,CAAC;AACJ,CAAC;AACD,SAAS,MAAM,CAAC,KAAiB;IAC/B,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,IAAI,MAAM,GAAG,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,EAAE,MAAM,IAAI,IAAI;QACxD,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;IAC1E,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACnF,CAAC;AACD,MAAM,UAAU,YAAY,CAAC,MAAmB;IAC9C,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,UAAU,EAAE,MAAM,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC;IACtF,MAAM,UAAU,GAAG,QAAQ,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;IACpD,MAAM,EAAE,GAAG,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,MAAM,CAAC,SAAS,CAAC;IAC9B,IAAI,OAAO,GAAG,IAAI,UAAU,EAAE,CAAC;IAC/B,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,IAAI,IAAI,EAAE,CAAC;QACT,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YAClB,KAAK,QAAQ,EAAE,CAAC;gBACd,GAAG,GAAG,CAAC,CAAC;gBACR,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAClC,OAAO,GAAG,IAAI,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;gBAC1C,IAAI,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;gBACtD,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;gBACrB,MAAM;YACR,CAAC;YACD,KAAK,MAAM;gBACT,GAAG,GAAG,CAAC,CAAC;gBACR,OAAO,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;gBAC5B,IAAI,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;gBAC9D,MAAM;YACR,KAAK,QAAQ;gBACX,GAAG,GAAG,CAAC,CAAC;gBACR,OAAO,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;gBAC5B,IAAI,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;gBACvD,MAAM;YACR,KAAK,SAAS;gBACZ,GAAG,GAAG,CAAC,CAAC;gBACR,OAAO,GAAG,IAAI,UAAU,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC/C,MAAM;YACR,KAAK,WAAW;gBACd,IACE,IAAI,CAAC,OAAO,GAAG,CAAC,kBAAkB;oBAClC,IAAI,CAAC,OAAO,GAAG,kBAAkB;oBACjC,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC;oBAC7B,IAAI,CAAC,KAAK,GAAG,CAAC;oBACd,IAAI,CAAC,KAAK,GAAG,SAAS;oBAEtB,MAAM,IAAI,SAAS,CAAC,mBAAmB,CAAC,CAAC;gBAC3C,GAAG,GAAG,CAAC,CAAC;gBACR,OAAO,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;gBAC7B,IAAI,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;gBAC1D,IAAI,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;gBACrD,MAAM;QACV,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,EAAE,GAAG,UAAU,CAAC,MAAM,GAAG,EAAE,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClF,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACxC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACb,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;IACpC,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;IACzB,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC;IAChD,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IACrC,KAAK,CAAC,CAAC,GAAG,UAAU,CAAC,MAAM,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC;IAC/C,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,GAAG,UAAU,CAAC,MAAM,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC;IACvD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AACD,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,IAAI,CAAC;QACH,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,MAAM,CAAC;YAAE,MAAM,IAAI,SAAS,EAAE,CAAC;QAChG,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;QACtE,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QAChE,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACxC,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,MAAM,IAAI,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;QACnD,MAAM,MAAM,GAAG,GAAW,EAAE;YAC1B,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YACrC,MAAM,IAAI,CAAC,CAAC;YACZ,IAAI,MAAM,GAAG,CAAC,IAAI,MAAM,GAAG,KAAK,CAAC,MAAM,GAAG,MAAM;gBAAE,MAAM,IAAI,SAAS,EAAE,CAAC;YACxE,MAAM,KAAK,GAAG,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,CAChE,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CACxC,CAAC;YACF,MAAM,IAAI,MAAM,CAAC;YACjB,OAAO,KAAK,CAAC;QACf,CAAC,CAAC;QACF,IAAI,IAAI,EAAE,KAAK,CAAC;YAAE,MAAM,IAAI,SAAS,EAAE,CAAC;QACxC,MAAM,eAAe,GAAG,GAAG,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QAChD,IAAI,SAAiC,CAAC;QACtC,QAAQ,IAAI,EAAE,EAAE,CAAC;YACf,KAAK,CAAC;gBACJ,MAAM;YACR,KAAK,CAAC;gBACJ,SAAS,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC;gBAChD,MAAM;YACR,KAAK,CAAC;gBACJ,SAAS,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC9D,MAAM,IAAI,CAAC,CAAC;gBACZ,MAAM;YACR,KAAK,CAAC;gBACJ,SAAS,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC/D,MAAM,IAAI,CAAC,CAAC;gBACZ,MAAM;YACR,KAAK,CAAC,EAAE,CAAC;gBACP,MAAM,KAAK,GAAG,IAAI,EAAE,CAAC;gBACrB,IAAI,KAAK,GAAG,CAAC;oBAAE,MAAM,IAAI,SAAS,EAAE,CAAC;gBACrC,SAAS,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,KAAK,CAAC,EAAE,CAAC;gBACpD,MAAM;YACR,CAAC;YACD,KAAK,CAAC,EAAE,CAAC;gBACP,IAAI,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;gBACvC,MAAM,IAAI,CAAC,CAAC;gBACZ,IAAI,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;gBAClC,MAAM,IAAI,CAAC,CAAC;gBACZ,2DAA2D;gBAC3D,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,aAAa,CAAC,CAAC;gBAChD,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC;gBACzB,KAAK,IAAI,KAAK,GAAG,aAAa,CAAC;gBAC/B,IAAI,OAAO,GAAG,CAAC,kBAAkB,IAAI,OAAO,GAAG,kBAAkB;oBAAE,MAAM,IAAI,SAAS,EAAE,CAAC;gBACzF,SAAS,GAAG,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;gBAClD,MAAM;YACR,CAAC;YACD;gBACE,MAAM,IAAI,SAAS,EAAE,CAAC;QAC1B,CAAC;QACD,IAAI,MAAM,KAAK,KAAK,CAAC,MAAM;YAAE,MAAM,IAAI,SAAS,EAAE,CAAC;QACnD,OAAO,MAAM,CAAC,MAAM,CAAC;YACnB,eAAe;YACf,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9D,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,SAAS,CAAC,gBAAgB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACnD,CAAC;AACH,CAAC","sourcesContent":["import { key, long } from './model.js';\nimport type { IndexCursor, IndexValue } from './model.js';\n\n/** Java UTF-8 replaces unpaired UTF-16 surrogates with '?'. */\nexport function javaUtf8(value: string): Uint8Array {\n return new TextEncoder().encode(\n value.replace(/[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDFFF]/g, (part) =>\n part.length === 2 ? part : '?',\n ),\n );\n}\nfunction base64(bytes: Uint8Array): string {\n let binary = '';\n for (let offset = 0; offset < bytes.length; offset += 8192)\n binary += String.fromCharCode(...bytes.subarray(offset, offset + 8192));\n return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');\n}\nexport function encodeCursor(cursor: IndexCursor): string {\n const documentKey = key(cursor.lastDocumentKey.collection, cursor.lastDocumentKey.id);\n const collection = javaUtf8(documentKey.collection);\n const id = javaUtf8(documentKey.id);\n const sort = cursor.sortValue;\n let payload = new Uint8Array();\n let tag = 0;\n if (sort) {\n switch (sort.type) {\n case 'string': {\n tag = 1;\n const text = javaUtf8(sort.value);\n payload = new Uint8Array(4 + text.length);\n new DataView(payload.buffer).setInt32(0, text.length);\n payload.set(text, 4);\n break;\n }\n case 'long':\n tag = 2;\n payload = new Uint8Array(8);\n new DataView(payload.buffer).setBigInt64(0, long(sort.value));\n break;\n case 'double':\n tag = 3;\n payload = new Uint8Array(8);\n new DataView(payload.buffer).setFloat64(0, sort.value);\n break;\n case 'boolean':\n tag = 4;\n payload = new Uint8Array([sort.value ? 1 : 0]);\n break;\n case 'timestamp':\n if (\n sort.seconds < -31557014167219200n ||\n sort.seconds > 31556889864403199n ||\n !Number.isInteger(sort.nanos) ||\n sort.nanos < 0 ||\n sort.nanos > 999999999\n )\n throw new TypeError('invalid timestamp');\n tag = 5;\n payload = new Uint8Array(12);\n new DataView(payload.buffer).setBigInt64(0, sort.seconds);\n new DataView(payload.buffer).setInt32(8, sort.nanos);\n break;\n }\n }\n const bytes = new Uint8Array(10 + collection.length + id.length + payload.length);\n const view = new DataView(bytes.buffer);\n bytes[0] = 1;\n view.setInt32(1, collection.length);\n bytes.set(collection, 5);\n view.setInt32(5 + collection.length, id.length);\n bytes.set(id, 9 + collection.length);\n bytes[9 + collection.length + id.length] = tag;\n bytes.set(payload, 10 + collection.length + id.length);\n return base64(bytes);\n}\nexport function decodeCursor(cursor: string): IndexCursor {\n try {\n if (typeof cursor !== 'string' || !/^[A-Za-z0-9_-]+={0,2}$/.test(cursor)) throw new TypeError();\n const binary = atob(cursor.replaceAll('-', '+').replaceAll('_', '/'));\n const bytes = Uint8Array.from(binary, (ch) => ch.charCodeAt(0));\n const view = new DataView(bytes.buffer);\n let offset = 0;\n const byte = (): number => view.getUint8(offset++);\n const string = (): string => {\n const length = view.getInt32(offset);\n offset += 4;\n if (length < 0 || length > bytes.length - offset) throw new TypeError();\n const value = new TextDecoder('utf-8', { ignoreBOM: true }).decode(\n bytes.subarray(offset, offset + length),\n );\n offset += length;\n return value;\n };\n if (byte() !== 1) throw new TypeError();\n const lastDocumentKey = key(string(), string());\n let sortValue: IndexValue | undefined;\n switch (byte()) {\n case 0:\n break;\n case 1:\n sortValue = { type: 'string', value: string() };\n break;\n case 2:\n sortValue = { type: 'long', value: view.getBigInt64(offset) };\n offset += 8;\n break;\n case 3:\n sortValue = { type: 'double', value: view.getFloat64(offset) };\n offset += 8;\n break;\n case 4: {\n const value = byte();\n if (value > 1) throw new TypeError();\n sortValue = { type: 'boolean', value: value === 1 };\n break;\n }\n case 5: {\n let seconds = view.getBigInt64(offset);\n offset += 8;\n let nanos = view.getInt32(offset);\n offset += 4;\n // Instant.ofEpochSecond normalizes nanosecond adjustments.\n const carry = Math.floor(nanos / 1_000_000_000);\n seconds += BigInt(carry);\n nanos -= carry * 1_000_000_000;\n if (seconds < -31557014167219200n || seconds > 31556889864403199n) throw new TypeError();\n sortValue = { type: 'timestamp', seconds, nanos };\n break;\n }\n default:\n throw new TypeError();\n }\n if (offset !== bytes.length) throw new TypeError();\n return Object.freeze({\n lastDocumentKey,\n ...(sortValue ? { sortValue: Object.freeze(sortValue) } : {}),\n });\n } catch (cause) {\n throw new TypeError('invalid cursor', { cause });\n }\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DocumentKey } from './model.js';
|
|
2
|
+
export declare class StorageError extends Error {
|
|
3
|
+
constructor(message: string, options?: ErrorOptions);
|
|
4
|
+
}
|
|
5
|
+
export declare class StorageAccessError extends StorageError {
|
|
6
|
+
}
|
|
7
|
+
export declare class StorageUnavailableError extends StorageError {
|
|
8
|
+
}
|
|
9
|
+
export declare class StorageOperationError extends StorageError {
|
|
10
|
+
}
|
|
11
|
+
export declare class DocumentNotFoundError extends StorageError {
|
|
12
|
+
readonly documentKey: DocumentKey;
|
|
13
|
+
constructor(documentKey: DocumentKey);
|
|
14
|
+
}
|
|
15
|
+
export declare class PersistenceError extends Error {
|
|
16
|
+
constructor(message: string, options?: ErrorOptions);
|
|
17
|
+
}
|
|
18
|
+
export declare class PartialFailureError extends PersistenceError {
|
|
19
|
+
readonly documentKey: DocumentKey;
|
|
20
|
+
constructor(message: string, documentKey: DocumentKey, cause: unknown);
|
|
21
|
+
}
|
|
22
|
+
export declare class InconsistentStateError extends PersistenceError {
|
|
23
|
+
readonly documentKey: DocumentKey;
|
|
24
|
+
constructor(message: string, documentKey: DocumentKey, cause?: unknown);
|
|
25
|
+
}
|
|
26
|
+
export declare class AdmissionRejectedError extends PersistenceError {
|
|
27
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export class StorageError extends Error {
|
|
2
|
+
constructor(message, options) {
|
|
3
|
+
super(message, options);
|
|
4
|
+
this.name = new.target.name;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class StorageAccessError extends StorageError {
|
|
8
|
+
}
|
|
9
|
+
export class StorageUnavailableError extends StorageError {
|
|
10
|
+
}
|
|
11
|
+
export class StorageOperationError extends StorageError {
|
|
12
|
+
}
|
|
13
|
+
export class DocumentNotFoundError extends StorageError {
|
|
14
|
+
documentKey;
|
|
15
|
+
constructor(documentKey) {
|
|
16
|
+
super(`Document not found: ${documentKey.collection}/${documentKey.id}`);
|
|
17
|
+
this.documentKey = documentKey;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class PersistenceError extends Error {
|
|
21
|
+
constructor(message, options) {
|
|
22
|
+
super(message, options);
|
|
23
|
+
this.name = new.target.name;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class PartialFailureError extends PersistenceError {
|
|
27
|
+
documentKey;
|
|
28
|
+
constructor(message, documentKey, cause) {
|
|
29
|
+
super(message, { cause });
|
|
30
|
+
this.documentKey = documentKey;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class InconsistentStateError extends PersistenceError {
|
|
34
|
+
documentKey;
|
|
35
|
+
constructor(message, documentKey, cause) {
|
|
36
|
+
super(message, { cause });
|
|
37
|
+
this.documentKey = documentKey;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export class AdmissionRejectedError extends PersistenceError {
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,YAAa,SAAQ,KAAK;IACrC,YAAY,OAAe,EAAE,OAAsB;QACjD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC;IAC9B,CAAC;CACF;AACD,MAAM,OAAO,kBAAmB,SAAQ,YAAY;CAAG;AACvD,MAAM,OAAO,uBAAwB,SAAQ,YAAY;CAAG;AAC5D,MAAM,OAAO,qBAAsB,SAAQ,YAAY;CAAG;AAC1D,MAAM,OAAO,qBAAsB,SAAQ,YAAY;IAChC,WAAW;IAAhC,YAAqB,WAAwB;QAC3C,KAAK,CAAC,uBAAuB,WAAW,CAAC,UAAU,IAAI,WAAW,CAAC,EAAE,EAAE,CAAC,CAAC;2BADtD,WAAW;IAEhC,CAAC;CACF;AACD,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IACzC,YAAY,OAAe,EAAE,OAAsB;QACjD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC;IAC9B,CAAC;CACF;AACD,MAAM,OAAO,mBAAoB,SAAQ,gBAAgB;IAG5C,WAAW;IAFtB,YACE,OAAe,EACN,WAAwB,EACjC,KAAc;QAEd,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;2BAHjB,WAAW;IAItB,CAAC;CACF;AACD,MAAM,OAAO,sBAAuB,SAAQ,gBAAgB;IAG/C,WAAW;IAFtB,YACE,OAAe,EACN,WAAwB,EACjC,KAAe;QAEf,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;2BAHjB,WAAW;IAItB,CAAC;CACF;AACD,MAAM,OAAO,sBAAuB,SAAQ,gBAAgB;CAAG","sourcesContent":["import type { DocumentKey } from './model.js';\n\nexport class StorageError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = new.target.name;\n }\n}\nexport class StorageAccessError extends StorageError {}\nexport class StorageUnavailableError extends StorageError {}\nexport class StorageOperationError extends StorageError {}\nexport class DocumentNotFoundError extends StorageError {\n constructor(readonly documentKey: DocumentKey) {\n super(`Document not found: ${documentKey.collection}/${documentKey.id}`);\n }\n}\nexport class PersistenceError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = new.target.name;\n }\n}\nexport class PartialFailureError extends PersistenceError {\n constructor(\n message: string,\n readonly documentKey: DocumentKey,\n cause: unknown,\n ) {\n super(message, { cause });\n }\n}\nexport class InconsistentStateError extends PersistenceError {\n constructor(\n message: string,\n readonly documentKey: DocumentKey,\n cause?: unknown,\n ) {\n super(message, { cause });\n }\n}\nexport class AdmissionRejectedError extends PersistenceError {}\n"]}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export interface DocumentKey {
|
|
2
|
+
readonly collection: string;
|
|
3
|
+
readonly id: string;
|
|
4
|
+
}
|
|
5
|
+
export type IndexType = 'string' | 'long' | 'double' | 'boolean';
|
|
6
|
+
export type IndexValue = {
|
|
7
|
+
readonly type: 'string';
|
|
8
|
+
readonly value: string;
|
|
9
|
+
} | {
|
|
10
|
+
readonly type: 'long';
|
|
11
|
+
readonly value: bigint;
|
|
12
|
+
} | {
|
|
13
|
+
readonly type: 'double';
|
|
14
|
+
readonly value: number;
|
|
15
|
+
} | {
|
|
16
|
+
readonly type: 'boolean';
|
|
17
|
+
readonly value: boolean;
|
|
18
|
+
} | {
|
|
19
|
+
readonly type: 'timestamp';
|
|
20
|
+
readonly seconds: bigint;
|
|
21
|
+
readonly nanos: number;
|
|
22
|
+
};
|
|
23
|
+
export interface IndexCursor {
|
|
24
|
+
readonly lastDocumentKey: DocumentKey;
|
|
25
|
+
readonly sortValue?: IndexValue;
|
|
26
|
+
}
|
|
27
|
+
export interface IndexEntry {
|
|
28
|
+
readonly documentKey: DocumentKey;
|
|
29
|
+
readonly values: Readonly<Record<string, IndexValue>>;
|
|
30
|
+
}
|
|
31
|
+
export type ComparisonOperator = '=' | '<>' | '>' | '>=' | '<' | '<=';
|
|
32
|
+
export type SortDirection = 'ASC' | 'DESC';
|
|
33
|
+
export interface Filter {
|
|
34
|
+
readonly field: string;
|
|
35
|
+
readonly operator: ComparisonOperator;
|
|
36
|
+
readonly value: IndexValue;
|
|
37
|
+
}
|
|
38
|
+
export interface Sort {
|
|
39
|
+
readonly field: string;
|
|
40
|
+
readonly direction: SortDirection;
|
|
41
|
+
}
|
|
42
|
+
export interface IndexQuery {
|
|
43
|
+
readonly collection: string;
|
|
44
|
+
readonly filters: readonly Filter[];
|
|
45
|
+
readonly limit: number;
|
|
46
|
+
readonly sort?: Sort;
|
|
47
|
+
readonly cursor?: IndexCursor;
|
|
48
|
+
}
|
|
49
|
+
export interface IndexPage {
|
|
50
|
+
readonly documentKeys: readonly DocumentKey[];
|
|
51
|
+
readonly nextCursor?: IndexCursor;
|
|
52
|
+
}
|
|
53
|
+
export interface DocumentPage {
|
|
54
|
+
readonly documentKeys: readonly DocumentKey[];
|
|
55
|
+
readonly nextCursor?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface Page<T> {
|
|
58
|
+
readonly items: readonly T[];
|
|
59
|
+
readonly nextCursor?: string;
|
|
60
|
+
}
|
|
61
|
+
/** Serialized bytes with defensive copies on input and output. */
|
|
62
|
+
export declare class StoredDocument {
|
|
63
|
+
#private;
|
|
64
|
+
constructor(content: Uint8Array);
|
|
65
|
+
content(): Uint8Array;
|
|
66
|
+
}
|
|
67
|
+
export interface DocumentStore {
|
|
68
|
+
put(key: DocumentKey, document: StoredDocument): Promise<void>;
|
|
69
|
+
get(key: DocumentKey): Promise<StoredDocument>;
|
|
70
|
+
delete(key: DocumentKey): Promise<void>;
|
|
71
|
+
list(collection: string, cursor: string | undefined, limit: number): Promise<DocumentPage>;
|
|
72
|
+
}
|
|
73
|
+
export interface CollectionMetadata {
|
|
74
|
+
readonly collection: string;
|
|
75
|
+
readonly fields: readonly {
|
|
76
|
+
readonly name: string;
|
|
77
|
+
readonly type: IndexType;
|
|
78
|
+
}[];
|
|
79
|
+
}
|
|
80
|
+
export interface IndexStore {
|
|
81
|
+
initialize(metadata: CollectionMetadata): Promise<void>;
|
|
82
|
+
upsert(entry: IndexEntry): Promise<void>;
|
|
83
|
+
delete(key: DocumentKey): Promise<void>;
|
|
84
|
+
query(query: IndexQuery): Promise<IndexPage>;
|
|
85
|
+
clear(collection: string): Promise<void>;
|
|
86
|
+
}
|
|
87
|
+
export declare function text(value: unknown, name: string): string;
|
|
88
|
+
export declare function key(collection: string, id: string): DocumentKey;
|
|
89
|
+
export declare function positive(value: number, name: string, max?: number): number;
|
|
90
|
+
export declare function long(value: unknown): bigint;
|
|
91
|
+
export declare function indexValue(type: IndexType, value: unknown): IndexValue;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Serialized bytes with defensive copies on input and output. */
|
|
2
|
+
export class StoredDocument {
|
|
3
|
+
#bytes;
|
|
4
|
+
constructor(content) {
|
|
5
|
+
if (!(content instanceof Uint8Array))
|
|
6
|
+
throw new TypeError('content must be Uint8Array');
|
|
7
|
+
this.#bytes = new Uint8Array(content);
|
|
8
|
+
}
|
|
9
|
+
content() {
|
|
10
|
+
return this.#bytes.slice();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
// Java String.isBlank excludes nonbreaking spaces.
|
|
14
|
+
export function text(value, name) {
|
|
15
|
+
if (typeof value !== 'string' ||
|
|
16
|
+
/^[\u0009-\u000D\u001C-\u0020\u1680\u2000-\u2006\u2008-\u200A\u2028\u2029\u205F\u3000]*$/u.test(value)) {
|
|
17
|
+
throw new TypeError(`${name} must be a nonblank string`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
export function key(collection, id) {
|
|
22
|
+
return Object.freeze({ collection: text(collection, 'collection'), id: text(id, 'id') });
|
|
23
|
+
}
|
|
24
|
+
export function positive(value, name, max = 2_147_483_647) {
|
|
25
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > max)
|
|
26
|
+
throw new RangeError(`${name} must be a positive integer <= ${max}`);
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
export function long(value) {
|
|
30
|
+
if (typeof value !== 'bigint' || value < -(1n << 63n) || value > (1n << 63n) - 1n) {
|
|
31
|
+
throw new TypeError('long must be a signed 64-bit bigint');
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
export function indexValue(type, value) {
|
|
36
|
+
switch (type) {
|
|
37
|
+
case 'string':
|
|
38
|
+
if (typeof value === 'string')
|
|
39
|
+
return Object.freeze({ type, value });
|
|
40
|
+
break;
|
|
41
|
+
case 'long':
|
|
42
|
+
return Object.freeze({ type, value: long(value) });
|
|
43
|
+
case 'double':
|
|
44
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
45
|
+
return Object.freeze({ type, value });
|
|
46
|
+
break;
|
|
47
|
+
case 'boolean':
|
|
48
|
+
if (typeof value === 'boolean')
|
|
49
|
+
return Object.freeze({ type, value });
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
throw new TypeError(`index value must have type ${type}`);
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.js","sourceRoot":"","sources":["../../src/core/model.ts"],"names":[],"mappings":"AAkDA,kEAAkE;AAClE,MAAM,OAAO,cAAc;IAChB,MAAM,CAAa;IAC5B,YAAY,OAAmB;QAC7B,IAAI,CAAC,CAAC,OAAO,YAAY,UAAU,CAAC;YAAE,MAAM,IAAI,SAAS,CAAC,4BAA4B,CAAC,CAAC;QACxF,IAAI,CAAC,MAAM,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC;IACxC,CAAC;IACD,OAAO;QACL,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IAC7B,CAAC;CACF;AAmBD,mDAAmD;AACnD,MAAM,UAAU,IAAI,CAAC,KAAc,EAAE,IAAY;IAC/C,IACE,OAAO,KAAK,KAAK,QAAQ;QACzB,0FAA0F,CAAC,IAAI,CAC7F,KAAK,CACN,EACD,CAAC;QACD,MAAM,IAAI,SAAS,CAAC,GAAG,IAAI,4BAA4B,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AACD,MAAM,UAAU,GAAG,CAAC,UAAkB,EAAE,EAAU;IAChD,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;AAC3F,CAAC;AACD,MAAM,UAAU,QAAQ,CAAC,KAAa,EAAE,IAAY,EAAE,GAAG,GAAG,aAAa;IACvE,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,GAAG,GAAG;QAC3D,MAAM,IAAI,UAAU,CAAC,GAAG,IAAI,kCAAkC,GAAG,EAAE,CAAC,CAAC;IACvE,OAAO,KAAK,CAAC;AACf,CAAC;AACD,MAAM,UAAU,IAAI,CAAC,KAAc;IACjC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC;QAClF,MAAM,IAAI,SAAS,CAAC,qCAAqC,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AACD,MAAM,UAAU,UAAU,CAAC,IAAe,EAAE,KAAc;IACxD,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,QAAQ;YACX,IAAI,OAAO,KAAK,KAAK,QAAQ;gBAAE,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACrE,MAAM;QACR,KAAK,MAAM;YACT,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACrD,KAAK,QAAQ;YACX,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;gBACrD,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACxC,MAAM;QACR,KAAK,SAAS;YACZ,IAAI,OAAO,KAAK,KAAK,SAAS;gBAAE,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACtE,MAAM;IACV,CAAC;IACD,MAAM,IAAI,SAAS,CAAC,8BAA8B,IAAI,EAAE,CAAC,CAAC;AAC5D,CAAC","sourcesContent":["export interface DocumentKey {\n readonly collection: string;\n readonly id: string;\n}\nexport type IndexType = 'string' | 'long' | 'double' | 'boolean';\nexport type IndexValue =\n | { readonly type: 'string'; readonly value: string }\n | { readonly type: 'long'; readonly value: bigint }\n | { readonly type: 'double'; readonly value: number }\n | { readonly type: 'boolean'; readonly value: boolean }\n | { readonly type: 'timestamp'; readonly seconds: bigint; readonly nanos: number };\nexport interface IndexCursor {\n readonly lastDocumentKey: DocumentKey;\n readonly sortValue?: IndexValue;\n}\nexport interface IndexEntry {\n readonly documentKey: DocumentKey;\n readonly values: Readonly<Record<string, IndexValue>>;\n}\nexport type ComparisonOperator = '=' | '<>' | '>' | '>=' | '<' | '<=';\nexport type SortDirection = 'ASC' | 'DESC';\nexport interface Filter {\n readonly field: string;\n readonly operator: ComparisonOperator;\n readonly value: IndexValue;\n}\nexport interface Sort {\n readonly field: string;\n readonly direction: SortDirection;\n}\nexport interface IndexQuery {\n readonly collection: string;\n readonly filters: readonly Filter[];\n readonly limit: number;\n readonly sort?: Sort;\n readonly cursor?: IndexCursor;\n}\nexport interface IndexPage {\n readonly documentKeys: readonly DocumentKey[];\n readonly nextCursor?: IndexCursor;\n}\nexport interface DocumentPage {\n readonly documentKeys: readonly DocumentKey[];\n readonly nextCursor?: string;\n}\nexport interface Page<T> {\n readonly items: readonly T[];\n readonly nextCursor?: string;\n}\n\n/** Serialized bytes with defensive copies on input and output. */\nexport class StoredDocument {\n readonly #bytes: Uint8Array;\n constructor(content: Uint8Array) {\n if (!(content instanceof Uint8Array)) throw new TypeError('content must be Uint8Array');\n this.#bytes = new Uint8Array(content);\n }\n content(): Uint8Array {\n return this.#bytes.slice();\n }\n}\nexport interface DocumentStore {\n put(key: DocumentKey, document: StoredDocument): Promise<void>;\n get(key: DocumentKey): Promise<StoredDocument>;\n delete(key: DocumentKey): Promise<void>;\n list(collection: string, cursor: string | undefined, limit: number): Promise<DocumentPage>;\n}\nexport interface CollectionMetadata {\n readonly collection: string;\n readonly fields: readonly { readonly name: string; readonly type: IndexType }[];\n}\nexport interface IndexStore {\n initialize(metadata: CollectionMetadata): Promise<void>;\n upsert(entry: IndexEntry): Promise<void>;\n delete(key: DocumentKey): Promise<void>;\n query(query: IndexQuery): Promise<IndexPage>;\n clear(collection: string): Promise<void>;\n}\n\n// Java String.isBlank excludes nonbreaking spaces.\nexport function text(value: unknown, name: string): string {\n if (\n typeof value !== 'string' ||\n /^[\\u0009-\\u000D\\u001C-\\u0020\\u1680\\u2000-\\u2006\\u2008-\\u200A\\u2028\\u2029\\u205F\\u3000]*$/u.test(\n value,\n )\n ) {\n throw new TypeError(`${name} must be a nonblank string`);\n }\n return value;\n}\nexport function key(collection: string, id: string): DocumentKey {\n return Object.freeze({ collection: text(collection, 'collection'), id: text(id, 'id') });\n}\nexport function positive(value: number, name: string, max = 2_147_483_647): number {\n if (!Number.isSafeInteger(value) || value <= 0 || value > max)\n throw new RangeError(`${name} must be a positive integer <= ${max}`);\n return value;\n}\nexport function long(value: unknown): bigint {\n if (typeof value !== 'bigint' || value < -(1n << 63n) || value > (1n << 63n) - 1n) {\n throw new TypeError('long must be a signed 64-bit bigint');\n }\n return value;\n}\nexport function indexValue(type: IndexType, value: unknown): IndexValue {\n switch (type) {\n case 'string':\n if (typeof value === 'string') return Object.freeze({ type, value });\n break;\n case 'long':\n return Object.freeze({ type, value: long(value) });\n case 'double':\n if (typeof value === 'number' && Number.isFinite(value))\n return Object.freeze({ type, value });\n break;\n case 'boolean':\n if (typeof value === 'boolean') return Object.freeze({ type, value });\n break;\n }\n throw new TypeError(`index value must have type ${type}`);\n}\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Filter, IndexQuery, Page, Sort, SortDirection } from './model.js';
|
|
2
|
+
import type { CollectionSchema } from './schema.js';
|
|
3
|
+
interface State {
|
|
4
|
+
readonly filters: readonly Filter[];
|
|
5
|
+
readonly sort?: Sort;
|
|
6
|
+
readonly limit?: number;
|
|
7
|
+
readonly cursor?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class Query<T extends object> {
|
|
10
|
+
#private;
|
|
11
|
+
readonly schema: CollectionSchema<T>;
|
|
12
|
+
readonly execute: (query: IndexQuery) => Promise<Page<T>>;
|
|
13
|
+
constructor(schema: CollectionSchema<T>, execute: (query: IndexQuery) => Promise<Page<T>>, state?: State);
|
|
14
|
+
where<K extends keyof T & string>(field: K): Comparison<T, T[K]>;
|
|
15
|
+
sortBy(field: keyof T & string, direction?: SortDirection): Query<T>;
|
|
16
|
+
limit(limit: number): Query<T>;
|
|
17
|
+
after(cursor: string): Query<T>;
|
|
18
|
+
fetch(): Promise<Page<T>>;
|
|
19
|
+
private copy;
|
|
20
|
+
}
|
|
21
|
+
export interface Comparison<T extends object, V> {
|
|
22
|
+
eq(value: V): Query<T>;
|
|
23
|
+
notEq(value: V): Query<T>;
|
|
24
|
+
gt(value: V): Query<T>;
|
|
25
|
+
gte(value: V): Query<T>;
|
|
26
|
+
lt(value: V): Query<T>;
|
|
27
|
+
lte(value: V): Query<T>;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { decodeCursor } from './cursor.js';
|
|
2
|
+
import { positive, text } from './model.js';
|
|
3
|
+
export class Query {
|
|
4
|
+
schema;
|
|
5
|
+
execute;
|
|
6
|
+
#state;
|
|
7
|
+
constructor(schema, execute, state = { filters: [] }) {
|
|
8
|
+
this.schema = schema;
|
|
9
|
+
this.execute = execute;
|
|
10
|
+
this.#state = Object.freeze({ ...state, filters: Object.freeze([...state.filters]) });
|
|
11
|
+
Object.freeze(this);
|
|
12
|
+
}
|
|
13
|
+
where(field) {
|
|
14
|
+
text(field, 'field');
|
|
15
|
+
const compare = (operator, value) => {
|
|
16
|
+
const indexed = this.schema.value(field, value);
|
|
17
|
+
if (indexed.type === 'boolean' && operator !== '=' && operator !== '<>')
|
|
18
|
+
throw new TypeError('boolean supports only equality comparisons');
|
|
19
|
+
const filter = Object.freeze({ field, operator, value: indexed });
|
|
20
|
+
return this.copy({ filters: [...this.#state.filters, filter] });
|
|
21
|
+
};
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
eq: (v) => compare('=', v),
|
|
24
|
+
notEq: (v) => compare('<>', v),
|
|
25
|
+
gt: (v) => compare('>', v),
|
|
26
|
+
gte: (v) => compare('>=', v),
|
|
27
|
+
lt: (v) => compare('<', v),
|
|
28
|
+
lte: (v) => compare('<=', v),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
sortBy(field, direction = 'ASC') {
|
|
32
|
+
if (this.#state.sort)
|
|
33
|
+
throw new TypeError('sort is already configured');
|
|
34
|
+
if (!this.schema.metadata.fields.some((f) => f.name === field))
|
|
35
|
+
throw new TypeError(`field is not indexed: ${field}`);
|
|
36
|
+
if (direction !== 'ASC' && direction !== 'DESC')
|
|
37
|
+
throw new TypeError('invalid sort direction');
|
|
38
|
+
return this.copy({ sort: Object.freeze({ field, direction }) });
|
|
39
|
+
}
|
|
40
|
+
limit(limit) {
|
|
41
|
+
if (this.#state.limit !== undefined)
|
|
42
|
+
throw new TypeError('limit is already configured');
|
|
43
|
+
return this.copy({ limit: positive(limit, 'limit') });
|
|
44
|
+
}
|
|
45
|
+
after(cursor) {
|
|
46
|
+
if (this.#state.cursor !== undefined)
|
|
47
|
+
throw new TypeError('cursor is already configured');
|
|
48
|
+
return this.copy({ cursor: text(cursor, 'cursor') });
|
|
49
|
+
}
|
|
50
|
+
async fetch() {
|
|
51
|
+
if (this.#state.limit === undefined)
|
|
52
|
+
throw new TypeError('limit must be configured before fetch');
|
|
53
|
+
return this.execute({
|
|
54
|
+
collection: this.schema.metadata.collection,
|
|
55
|
+
filters: this.#state.filters,
|
|
56
|
+
limit: this.#state.limit,
|
|
57
|
+
...(this.#state.sort ? { sort: this.#state.sort } : {}),
|
|
58
|
+
...(this.#state.cursor ? { cursor: decodeCursor(this.#state.cursor) } : {}),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
copy(update) {
|
|
62
|
+
return new Query(this.schema, this.execute, { ...this.#state, ...update });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/core/query.ts"],"names":[],"mappings":"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","sourcesContent":["import { decodeCursor } from './cursor.js';\nimport { positive, text } from './model.js';\nimport type { ComparisonOperator, Filter, IndexQuery, Page, Sort, SortDirection } from './model.js';\nimport type { CollectionSchema } from './schema.js';\n\ninterface State {\n readonly filters: readonly Filter[];\n readonly sort?: Sort;\n readonly limit?: number;\n readonly cursor?: string;\n}\nexport class Query<T extends object> {\n readonly #state: State;\n constructor(\n readonly schema: CollectionSchema<T>,\n readonly execute: (query: IndexQuery) => Promise<Page<T>>,\n state: State = { filters: [] },\n ) {\n this.#state = Object.freeze({ ...state, filters: Object.freeze([...state.filters]) });\n Object.freeze(this);\n }\n where<K extends keyof T & string>(field: K): Comparison<T, T[K]> {\n text(field, 'field');\n const compare = (operator: ComparisonOperator, value: T[K]): Query<T> => {\n const indexed = this.schema.value(field, value);\n if (indexed.type === 'boolean' && operator !== '=' && operator !== '<>')\n throw new TypeError('boolean supports only equality comparisons');\n const filter = Object.freeze({ field, operator, value: indexed });\n return this.copy({ filters: [...this.#state.filters, filter] });\n };\n return Object.freeze({\n eq: (v: T[K]) => compare('=', v),\n notEq: (v: T[K]) => compare('<>', v),\n gt: (v: T[K]) => compare('>', v),\n gte: (v: T[K]) => compare('>=', v),\n lt: (v: T[K]) => compare('<', v),\n lte: (v: T[K]) => compare('<=', v),\n });\n }\n sortBy(field: keyof T & string, direction: SortDirection = 'ASC'): Query<T> {\n if (this.#state.sort) throw new TypeError('sort is already configured');\n if (!this.schema.metadata.fields.some((f) => f.name === field))\n throw new TypeError(`field is not indexed: ${field}`);\n if (direction !== 'ASC' && direction !== 'DESC') throw new TypeError('invalid sort direction');\n return this.copy({ sort: Object.freeze({ field, direction }) });\n }\n limit(limit: number): Query<T> {\n if (this.#state.limit !== undefined) throw new TypeError('limit is already configured');\n return this.copy({ limit: positive(limit, 'limit') });\n }\n after(cursor: string): Query<T> {\n if (this.#state.cursor !== undefined) throw new TypeError('cursor is already configured');\n return this.copy({ cursor: text(cursor, 'cursor') });\n }\n async fetch(): Promise<Page<T>> {\n if (this.#state.limit === undefined)\n throw new TypeError('limit must be configured before fetch');\n return this.execute({\n collection: this.schema.metadata.collection,\n filters: this.#state.filters,\n limit: this.#state.limit,\n ...(this.#state.sort ? { sort: this.#state.sort } : {}),\n ...(this.#state.cursor ? { cursor: decodeCursor(this.#state.cursor) } : {}),\n });\n }\n private copy(update: Partial<State>): Query<T> {\n return new Query(this.schema, this.execute, { ...this.#state, ...update });\n }\n}\nexport interface Comparison<T extends object, V> {\n eq(value: V): Query<T>;\n notEq(value: V): Query<T>;\n gt(value: V): Query<T>;\n gte(value: V): Query<T>;\n lt(value: V): Query<T>;\n lte(value: V): Query<T>;\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CollectionMetadata, IndexEntry, IndexValue } from './model.js';
|
|
2
|
+
export type IndexedFields<T> = Partial<{
|
|
3
|
+
readonly [K in keyof T & string]: T[K] extends string ? 'string' : T[K] extends bigint ? 'long' : T[K] extends number ? 'double' : T[K] extends boolean ? 'boolean' : never;
|
|
4
|
+
}>;
|
|
5
|
+
export interface SchemaOptions<T extends object> {
|
|
6
|
+
readonly collection: string;
|
|
7
|
+
readonly id: {
|
|
8
|
+
[K in keyof T & string]: T[K] extends string ? K : never;
|
|
9
|
+
}[keyof T & string];
|
|
10
|
+
readonly indexes: IndexedFields<T>;
|
|
11
|
+
}
|
|
12
|
+
export declare function identifier(value: string): string;
|
|
13
|
+
export declare function validateMetadata(metadata: CollectionMetadata): CollectionMetadata;
|
|
14
|
+
export declare class CollectionSchema<T extends object> {
|
|
15
|
+
readonly metadata: CollectionMetadata;
|
|
16
|
+
readonly id: keyof T & string;
|
|
17
|
+
constructor(options: SchemaOptions<T>);
|
|
18
|
+
value(field: string, value: unknown): IndexValue;
|
|
19
|
+
entry(document: T): IndexEntry;
|
|
20
|
+
}
|
|
21
|
+
export declare function defineSchema<T extends object>(options: SchemaOptions<T>): CollectionSchema<T>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { indexValue, key } from './model.js';
|
|
2
|
+
export function identifier(value) {
|
|
3
|
+
if (typeof value !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(value))
|
|
4
|
+
throw new TypeError('identifier must match [A-Za-z_][A-Za-z0-9_]*');
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
export function validateMetadata(metadata) {
|
|
8
|
+
const collection = identifier(metadata.collection);
|
|
9
|
+
if (metadata.fields.length > 5)
|
|
10
|
+
throw new RangeError('at most five indexed fields are supported');
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
const fields = metadata.fields
|
|
13
|
+
.map((field) => {
|
|
14
|
+
const name = identifier(field.name);
|
|
15
|
+
if (name === 'document_id' || seen.has(name))
|
|
16
|
+
throw new TypeError('reserved or duplicate indexed field');
|
|
17
|
+
if (!['string', 'long', 'double', 'boolean'].includes(field.type))
|
|
18
|
+
throw new TypeError('unsupported index type');
|
|
19
|
+
seen.add(name);
|
|
20
|
+
return Object.freeze({ name, type: field.type });
|
|
21
|
+
})
|
|
22
|
+
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
23
|
+
return Object.freeze({ collection, fields: Object.freeze(fields) });
|
|
24
|
+
}
|
|
25
|
+
export class CollectionSchema {
|
|
26
|
+
metadata;
|
|
27
|
+
id;
|
|
28
|
+
constructor(options) {
|
|
29
|
+
identifier(options.id);
|
|
30
|
+
this.id = options.id;
|
|
31
|
+
this.metadata = validateMetadata({
|
|
32
|
+
collection: options.collection,
|
|
33
|
+
fields: Object.entries(options.indexes).map(([name, type]) => ({
|
|
34
|
+
name,
|
|
35
|
+
type: type,
|
|
36
|
+
})),
|
|
37
|
+
});
|
|
38
|
+
Object.freeze(this);
|
|
39
|
+
}
|
|
40
|
+
value(field, value) {
|
|
41
|
+
const definition = this.metadata.fields.find((f) => f.name === field);
|
|
42
|
+
if (!definition)
|
|
43
|
+
throw new TypeError(`field is not indexed: ${field}`);
|
|
44
|
+
return indexValue(definition.type, value);
|
|
45
|
+
}
|
|
46
|
+
entry(document) {
|
|
47
|
+
if (document === null || typeof document !== 'object')
|
|
48
|
+
throw new TypeError('document must be an object');
|
|
49
|
+
const values = Object.create(null);
|
|
50
|
+
for (const field of this.metadata.fields)
|
|
51
|
+
values[field.name] = this.value(field.name, document[field.name]);
|
|
52
|
+
return Object.freeze({
|
|
53
|
+
documentKey: key(this.metadata.collection, document[this.id]),
|
|
54
|
+
values: Object.freeze(values),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function defineSchema(options) {
|
|
59
|
+
return new CollectionSchema(options);
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/core/schema.ts"],"names":[],"mappings":"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","sourcesContent":["import { indexValue, key } from './model.js';\nimport type { CollectionMetadata, IndexEntry, IndexType, IndexValue } from './model.js';\n\nexport type IndexedFields<T> = Partial<{\n readonly [K in keyof T & string]: T[K] extends string\n ? 'string'\n : T[K] extends bigint\n ? 'long'\n : T[K] extends number\n ? 'double'\n : T[K] extends boolean\n ? 'boolean'\n : never;\n}>;\nexport interface SchemaOptions<T extends object> {\n readonly collection: string;\n readonly id: { [K in keyof T & string]: T[K] extends string ? K : never }[keyof T & string];\n readonly indexes: IndexedFields<T>;\n}\nexport function identifier(value: string): string {\n if (typeof value !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(value))\n throw new TypeError('identifier must match [A-Za-z_][A-Za-z0-9_]*');\n return value;\n}\nexport function validateMetadata(metadata: CollectionMetadata): CollectionMetadata {\n const collection = identifier(metadata.collection);\n if (metadata.fields.length > 5) throw new RangeError('at most five indexed fields are supported');\n const seen = new Set<string>();\n const fields = metadata.fields\n .map((field) => {\n const name = identifier(field.name);\n if (name === 'document_id' || seen.has(name))\n throw new TypeError('reserved or duplicate indexed field');\n if (!['string', 'long', 'double', 'boolean'].includes(field.type))\n throw new TypeError('unsupported index type');\n seen.add(name);\n return Object.freeze({ name, type: field.type });\n })\n .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));\n return Object.freeze({ collection, fields: Object.freeze(fields) });\n}\nexport class CollectionSchema<T extends object> {\n readonly metadata: CollectionMetadata;\n readonly id: keyof T & string;\n constructor(options: SchemaOptions<T>) {\n identifier(options.id);\n this.id = options.id;\n this.metadata = validateMetadata({\n collection: options.collection,\n fields: Object.entries(options.indexes).map(([name, type]) => ({\n name,\n type: type as IndexType,\n })),\n });\n Object.freeze(this);\n }\n value(field: string, value: unknown): IndexValue {\n const definition = this.metadata.fields.find((f) => f.name === field);\n if (!definition) throw new TypeError(`field is not indexed: ${field}`);\n return indexValue(definition.type, value);\n }\n entry(document: T): IndexEntry {\n if (document === null || typeof document !== 'object')\n throw new TypeError('document must be an object');\n const values = Object.create(null) as Record<string, IndexValue>;\n for (const field of this.metadata.fields)\n values[field.name] = this.value(field.name, document[field.name as keyof T]);\n return Object.freeze({\n documentKey: key(this.metadata.collection, document[this.id] as string),\n values: Object.freeze(values),\n });\n }\n}\nexport function defineSchema<T extends object>(options: SchemaOptions<T>): CollectionSchema<T> {\n return new CollectionSchema(options);\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { R2D1, R2D1Collection } from './core/collection.js';
|
|
2
|
+
export type { CollectionOptions } from './core/collection.js';
|
|
3
|
+
export { CollectionSchema, defineSchema } from './core/schema.js';
|
|
4
|
+
export type { SchemaOptions, IndexedFields } from './core/schema.js';
|
|
5
|
+
export { Query } from './core/query.js';
|
|
6
|
+
export type { Comparison } from './core/query.js';
|
|
7
|
+
export { jsonCodec } from './core/codec.js';
|
|
8
|
+
export type { DocumentCodec } from './core/codec.js';
|
|
9
|
+
export { StoredDocument } from './core/model.js';
|
|
10
|
+
export type { DocumentKey, DocumentStore, DocumentPage, CollectionMetadata, IndexStore, IndexValue, IndexType, IndexEntry, IndexCursor, IndexPage, IndexQuery, Filter, Sort, SortDirection, ComparisonOperator, Page, } from './core/model.js';
|
|
11
|
+
export * from './core/errors.js';
|
|
12
|
+
export { encodeCursor, decodeCursor } from './core/cursor.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { R2D1, R2D1Collection } from './core/collection.js';
|
|
2
|
+
export { CollectionSchema, defineSchema } from './core/schema.js';
|
|
3
|
+
export { Query } from './core/query.js';
|
|
4
|
+
export { jsonCodec } from './core/codec.js';
|
|
5
|
+
export { StoredDocument } from './core/model.js';
|
|
6
|
+
export * from './core/errors.js';
|
|
7
|
+
export { encodeCursor, decodeCursor } from './core/cursor.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|