@frostpillar/frostpillar-storage-engine 0.0.1

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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README-JA.md +1205 -0
  3. package/README.md +1204 -0
  4. package/dist/drivers/file.cjs +960 -0
  5. package/dist/drivers/file.d.ts +3 -0
  6. package/dist/drivers/file.js +18 -0
  7. package/dist/drivers/indexedDB.cjs +570 -0
  8. package/dist/drivers/indexedDB.d.ts +3 -0
  9. package/dist/drivers/indexedDB.js +18 -0
  10. package/dist/drivers/localStorage.cjs +668 -0
  11. package/dist/drivers/localStorage.d.ts +3 -0
  12. package/dist/drivers/localStorage.js +23 -0
  13. package/dist/drivers/opfs.cjs +550 -0
  14. package/dist/drivers/opfs.d.ts +3 -0
  15. package/dist/drivers/opfs.js +18 -0
  16. package/dist/drivers/syncStorage.cjs +898 -0
  17. package/dist/drivers/syncStorage.d.ts +3 -0
  18. package/dist/drivers/syncStorage.js +22 -0
  19. package/dist/drivers/validation.d.ts +1 -0
  20. package/dist/drivers/validation.js +8 -0
  21. package/dist/errors/index.d.ts +32 -0
  22. package/dist/errors/index.js +48 -0
  23. package/dist/frostpillar-storage-engine.min.js +1 -0
  24. package/dist/index.cjs +2957 -0
  25. package/dist/index.d.ts +7 -0
  26. package/dist/index.js +6 -0
  27. package/dist/storage/backend/asyncDurableAutoCommitController.d.ts +26 -0
  28. package/dist/storage/backend/asyncDurableAutoCommitController.js +188 -0
  29. package/dist/storage/backend/asyncMutex.d.ts +7 -0
  30. package/dist/storage/backend/asyncMutex.js +38 -0
  31. package/dist/storage/backend/autoCommit.d.ts +2 -0
  32. package/dist/storage/backend/autoCommit.js +22 -0
  33. package/dist/storage/backend/capacity.d.ts +2 -0
  34. package/dist/storage/backend/capacity.js +27 -0
  35. package/dist/storage/backend/capacityResolver.d.ts +3 -0
  36. package/dist/storage/backend/capacityResolver.js +25 -0
  37. package/dist/storage/backend/encoding.d.ts +17 -0
  38. package/dist/storage/backend/encoding.js +148 -0
  39. package/dist/storage/backend/types.d.ts +184 -0
  40. package/dist/storage/backend/types.js +1 -0
  41. package/dist/storage/btree/recordKeyIndexBTree.d.ts +39 -0
  42. package/dist/storage/btree/recordKeyIndexBTree.js +104 -0
  43. package/dist/storage/config/config.browser.d.ts +4 -0
  44. package/dist/storage/config/config.browser.js +8 -0
  45. package/dist/storage/config/config.d.ts +1 -0
  46. package/dist/storage/config/config.js +1 -0
  47. package/dist/storage/config/config.node.d.ts +4 -0
  48. package/dist/storage/config/config.node.js +74 -0
  49. package/dist/storage/config/config.shared.d.ts +6 -0
  50. package/dist/storage/config/config.shared.js +105 -0
  51. package/dist/storage/datastore/Datastore.d.ts +47 -0
  52. package/dist/storage/datastore/Datastore.js +525 -0
  53. package/dist/storage/datastore/datastoreClose.d.ts +12 -0
  54. package/dist/storage/datastore/datastoreClose.js +60 -0
  55. package/dist/storage/datastore/datastoreKeyDefinition.d.ts +7 -0
  56. package/dist/storage/datastore/datastoreKeyDefinition.js +60 -0
  57. package/dist/storage/datastore/datastoreLifecycle.d.ts +18 -0
  58. package/dist/storage/datastore/datastoreLifecycle.js +63 -0
  59. package/dist/storage/datastore/mutationById.d.ts +29 -0
  60. package/dist/storage/datastore/mutationById.js +71 -0
  61. package/dist/storage/drivers/IndexedDB/indexedDBBackend.d.ts +11 -0
  62. package/dist/storage/drivers/IndexedDB/indexedDBBackend.js +109 -0
  63. package/dist/storage/drivers/IndexedDB/indexedDBBackendController.d.ts +27 -0
  64. package/dist/storage/drivers/IndexedDB/indexedDBBackendController.js +60 -0
  65. package/dist/storage/drivers/IndexedDB/indexedDBConfig.d.ts +7 -0
  66. package/dist/storage/drivers/IndexedDB/indexedDBConfig.js +24 -0
  67. package/dist/storage/drivers/file/fileBackend.d.ts +5 -0
  68. package/dist/storage/drivers/file/fileBackend.js +168 -0
  69. package/dist/storage/drivers/file/fileBackendController.d.ts +31 -0
  70. package/dist/storage/drivers/file/fileBackendController.js +72 -0
  71. package/dist/storage/drivers/file/fileBackendSnapshot.d.ts +10 -0
  72. package/dist/storage/drivers/file/fileBackendSnapshot.js +166 -0
  73. package/dist/storage/drivers/localStorage/localStorageBackend.d.ts +10 -0
  74. package/dist/storage/drivers/localStorage/localStorageBackend.js +156 -0
  75. package/dist/storage/drivers/localStorage/localStorageBackendController.d.ts +24 -0
  76. package/dist/storage/drivers/localStorage/localStorageBackendController.js +35 -0
  77. package/dist/storage/drivers/localStorage/localStorageConfig.d.ts +10 -0
  78. package/dist/storage/drivers/localStorage/localStorageConfig.js +16 -0
  79. package/dist/storage/drivers/localStorage/localStorageLayout.d.ts +5 -0
  80. package/dist/storage/drivers/localStorage/localStorageLayout.js +29 -0
  81. package/dist/storage/drivers/opfs/opfsBackend.d.ts +12 -0
  82. package/dist/storage/drivers/opfs/opfsBackend.js +142 -0
  83. package/dist/storage/drivers/opfs/opfsBackendController.d.ts +26 -0
  84. package/dist/storage/drivers/opfs/opfsBackendController.js +44 -0
  85. package/dist/storage/drivers/syncStorage/syncStorageAdapter.d.ts +2 -0
  86. package/dist/storage/drivers/syncStorage/syncStorageAdapter.js +123 -0
  87. package/dist/storage/drivers/syncStorage/syncStorageBackend.d.ts +11 -0
  88. package/dist/storage/drivers/syncStorage/syncStorageBackend.js +169 -0
  89. package/dist/storage/drivers/syncStorage/syncStorageBackendController.d.ts +24 -0
  90. package/dist/storage/drivers/syncStorage/syncStorageBackendController.js +34 -0
  91. package/dist/storage/drivers/syncStorage/syncStorageChunkMaintenance.d.ts +2 -0
  92. package/dist/storage/drivers/syncStorage/syncStorageChunkMaintenance.js +28 -0
  93. package/dist/storage/drivers/syncStorage/syncStorageConfig.d.ts +13 -0
  94. package/dist/storage/drivers/syncStorage/syncStorageConfig.js +42 -0
  95. package/dist/storage/drivers/syncStorage/syncStorageQuota.d.ts +3 -0
  96. package/dist/storage/drivers/syncStorage/syncStorageQuota.js +45 -0
  97. package/dist/storage/record/ordering.d.ts +3 -0
  98. package/dist/storage/record/ordering.js +7 -0
  99. package/dist/types.d.ts +125 -0
  100. package/dist/types.js +1 -0
  101. package/dist/validation/metadata.d.ts +1 -0
  102. package/dist/validation/metadata.js +7 -0
  103. package/dist/validation/payload.d.ts +7 -0
  104. package/dist/validation/payload.js +135 -0
  105. package/dist/validation/typeGuards.d.ts +1 -0
  106. package/dist/validation/typeGuards.js +7 -0
  107. package/package.json +110 -0
@@ -0,0 +1,7 @@
1
+ export { Datastore } from './storage/datastore/Datastore.js';
2
+ export type { AutoCommitConfig, AutoCommitFrequencyInput, CapacityConfig, CapacityPolicy, DatastoreConfig, DuplicateKeyPolicy, DatastoreDriver, DatastoreDriverController, DatastoreDriverInitContext, DatastoreDriverInitResult, DatastoreDriverSnapshot, DatastoreErrorEvent, DatastoreErrorListener, DatastoreKeyDefinition, EntryId, FileBackendConfig, FileTargetByDirectoryConfig, FileTargetByPathConfig, FileTargetConfig, InputRecord, IndexedDBConfig, KeyedRecord, LocalStorageConfig, OpfsConfig, PersistedRecord, RecordPayload, SyncStorageConfig, } from './types.js';
3
+ export { BinaryFormatError, ClosedDatastoreError, ConfigurationError, DatabaseLockedError, FrostpillarError, IndexCorruptionError, InvalidQueryRangeError, PageCorruptionError, QuotaExceededError, StorageEngineError, UnsupportedBackendError, ValidationError, } from './errors/index.js';
4
+ export { localStorageDriver } from './drivers/localStorage.js';
5
+ export { indexedDBDriver } from './drivers/indexedDB.js';
6
+ export { opfsDriver } from './drivers/opfs.js';
7
+ export { syncStorageDriver } from './drivers/syncStorage.js';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { Datastore } from './storage/datastore/Datastore.js';
2
+ export { BinaryFormatError, ClosedDatastoreError, ConfigurationError, DatabaseLockedError, FrostpillarError, IndexCorruptionError, InvalidQueryRangeError, PageCorruptionError, QuotaExceededError, StorageEngineError, UnsupportedBackendError, ValidationError, } from './errors/index.js';
3
+ export { localStorageDriver } from './drivers/localStorage.js';
4
+ export { indexedDBDriver } from './drivers/indexedDB.js';
5
+ export { opfsDriver } from './drivers/opfs.js';
6
+ export { syncStorageDriver } from './drivers/syncStorage.js';
@@ -0,0 +1,26 @@
1
+ import type { FileAutoCommitState } from './types.js';
2
+ export declare abstract class AsyncDurableAutoCommitController {
3
+ private readonly autoCommit;
4
+ private readonly onAutoCommitError;
5
+ private pendingAutoCommitBytes;
6
+ private dirtyFromClear;
7
+ private autoCommitTimer;
8
+ private commitInFlight;
9
+ private pendingForegroundCommitRequest;
10
+ private pendingBackgroundCommitRequest;
11
+ private closed;
12
+ protected constructor(autoCommit: FileAutoCommitState, onAutoCommitError: (error: unknown) => void);
13
+ handleRecordAppended(encodedBytes: number): Promise<void>;
14
+ handleCleared(): Promise<void>;
15
+ commitNow(): Promise<void>;
16
+ close(): Promise<void>;
17
+ protected getPendingAutoCommitBytes(): number;
18
+ protected abstract executeSingleCommit(): Promise<void>;
19
+ protected onCloseAfterDrain(): Promise<void>;
20
+ private waitForCommitSettlement;
21
+ private queueCommitRequest;
22
+ private runCommitLoop;
23
+ private startAutoCommitSchedule;
24
+ private stopAutoCommitSchedule;
25
+ private handleAutoCommitTick;
26
+ }
@@ -0,0 +1,188 @@
1
+ import { toErrorInstance } from '../../errors/index.js';
2
+ export class AsyncDurableAutoCommitController {
3
+ autoCommit;
4
+ onAutoCommitError;
5
+ pendingAutoCommitBytes;
6
+ dirtyFromClear;
7
+ autoCommitTimer;
8
+ commitInFlight;
9
+ pendingForegroundCommitRequest;
10
+ pendingBackgroundCommitRequest;
11
+ closed;
12
+ constructor(autoCommit, onAutoCommitError) {
13
+ this.autoCommit = autoCommit;
14
+ this.onAutoCommitError = onAutoCommitError;
15
+ this.pendingAutoCommitBytes = 0;
16
+ this.dirtyFromClear = false;
17
+ this.autoCommitTimer = null;
18
+ this.commitInFlight = null;
19
+ this.pendingForegroundCommitRequest = false;
20
+ this.pendingBackgroundCommitRequest = false;
21
+ this.closed = false;
22
+ this.startAutoCommitSchedule();
23
+ }
24
+ handleRecordAppended(encodedBytes) {
25
+ if (this.autoCommit.frequency === 'immediate') {
26
+ return this.commitNow();
27
+ }
28
+ this.pendingAutoCommitBytes += encodedBytes;
29
+ if (this.autoCommit.maxPendingBytes !== null &&
30
+ this.pendingAutoCommitBytes >= this.autoCommit.maxPendingBytes) {
31
+ return this.queueCommitRequest('foreground');
32
+ }
33
+ return Promise.resolve();
34
+ }
35
+ handleCleared() {
36
+ this.dirtyFromClear = true;
37
+ if (this.autoCommit.frequency === 'immediate') {
38
+ return this.commitNow();
39
+ }
40
+ return this.queueCommitRequest('background');
41
+ }
42
+ commitNow() {
43
+ return this.queueCommitRequest('foreground');
44
+ }
45
+ async close() {
46
+ if (this.closed) {
47
+ return;
48
+ }
49
+ this.closed = true;
50
+ this.stopAutoCommitSchedule();
51
+ await this.waitForCommitSettlement();
52
+ let flushError = null;
53
+ if (this.pendingAutoCommitBytes > 0 || this.dirtyFromClear) {
54
+ try {
55
+ await this.executeSingleCommit();
56
+ this.pendingAutoCommitBytes = 0;
57
+ this.dirtyFromClear = false;
58
+ }
59
+ catch (error) {
60
+ flushError = toErrorInstance(error, 'Final close-time flush commit failed with a non-Error value.');
61
+ }
62
+ }
63
+ let drainError = null;
64
+ try {
65
+ await this.onCloseAfterDrain();
66
+ }
67
+ catch (error) {
68
+ drainError = toErrorInstance(error, 'onCloseAfterDrain failed with a non-Error value.');
69
+ }
70
+ if (flushError !== null && drainError !== null) {
71
+ throw createCloseAggregateError(flushError, drainError);
72
+ }
73
+ if (flushError !== null) {
74
+ throw flushError;
75
+ }
76
+ if (drainError !== null) {
77
+ throw drainError;
78
+ }
79
+ }
80
+ getPendingAutoCommitBytes() {
81
+ return this.pendingAutoCommitBytes;
82
+ }
83
+ onCloseAfterDrain() {
84
+ return Promise.resolve();
85
+ }
86
+ waitForCommitSettlement() {
87
+ if (this.commitInFlight === null) {
88
+ return Promise.resolve();
89
+ }
90
+ return this.commitInFlight
91
+ .then(() => undefined)
92
+ .catch(() => undefined);
93
+ }
94
+ queueCommitRequest(requestType) {
95
+ if (requestType === 'foreground') {
96
+ this.pendingForegroundCommitRequest = true;
97
+ }
98
+ else {
99
+ this.pendingBackgroundCommitRequest = true;
100
+ }
101
+ if (this.commitInFlight === null) {
102
+ this.commitInFlight = this.runCommitLoop().finally(() => {
103
+ this.commitInFlight = null;
104
+ });
105
+ }
106
+ if (requestType === 'background') {
107
+ return Promise.resolve();
108
+ }
109
+ return this.commitInFlight;
110
+ }
111
+ async runCommitLoop() {
112
+ let shouldContinue = true;
113
+ while (shouldContinue) {
114
+ const runForeground = this.pendingForegroundCommitRequest;
115
+ const runBackground = this.pendingBackgroundCommitRequest;
116
+ const runClear = this.dirtyFromClear;
117
+ this.pendingForegroundCommitRequest = false;
118
+ this.pendingBackgroundCommitRequest = false;
119
+ this.dirtyFromClear = false;
120
+ const shouldRunCommit = runForeground || (runBackground && (this.pendingAutoCommitBytes > 0 || runClear));
121
+ if (!shouldRunCommit) {
122
+ shouldContinue = false;
123
+ continue;
124
+ }
125
+ try {
126
+ const committedPendingBytes = this.pendingAutoCommitBytes;
127
+ await this.executeSingleCommit();
128
+ this.pendingAutoCommitBytes = Math.max(0, this.pendingAutoCommitBytes - committedPendingBytes);
129
+ }
130
+ catch (error) {
131
+ if (runClear) {
132
+ this.dirtyFromClear = true;
133
+ }
134
+ if (runForeground) {
135
+ throw toErrorInstance(error, 'Foreground auto-commit failed with a non-Error value.');
136
+ }
137
+ this.onAutoCommitError(error);
138
+ }
139
+ if (!this.pendingForegroundCommitRequest && !this.pendingBackgroundCommitRequest) {
140
+ shouldContinue = false;
141
+ }
142
+ }
143
+ }
144
+ startAutoCommitSchedule() {
145
+ if (this.autoCommit.frequency !== 'scheduled' ||
146
+ this.autoCommit.intervalMs === null) {
147
+ return;
148
+ }
149
+ this.autoCommitTimer = setInterval(() => {
150
+ this.handleAutoCommitTick();
151
+ }, this.autoCommit.intervalMs);
152
+ if (typeof this.autoCommitTimer === 'object' && this.autoCommitTimer !== null && 'unref' in this.autoCommitTimer) {
153
+ this.autoCommitTimer.unref();
154
+ }
155
+ }
156
+ stopAutoCommitSchedule() {
157
+ if (this.autoCommitTimer === null) {
158
+ return;
159
+ }
160
+ clearInterval(this.autoCommitTimer);
161
+ this.autoCommitTimer = null;
162
+ }
163
+ handleAutoCommitTick() {
164
+ if (this.closed) {
165
+ return;
166
+ }
167
+ if (this.pendingAutoCommitBytes <= 0 && !this.dirtyFromClear) {
168
+ return;
169
+ }
170
+ void this.queueCommitRequest('background');
171
+ }
172
+ }
173
+ const readAggregateErrorConstructor = () => {
174
+ const candidate = globalThis.AggregateError;
175
+ if (typeof candidate !== 'function') {
176
+ return null;
177
+ }
178
+ return candidate;
179
+ };
180
+ const createCloseAggregateError = (flushError, drainError) => {
181
+ const aggregateErrorConstructor = readAggregateErrorConstructor();
182
+ if (aggregateErrorConstructor !== null) {
183
+ return new aggregateErrorConstructor([flushError, drainError], 'Close failed: both final flush and drain produced errors.');
184
+ }
185
+ const fallbackError = new Error('Close failed: both final flush and drain produced errors.');
186
+ fallbackError.errors = [flushError, drainError];
187
+ return fallbackError;
188
+ };
@@ -0,0 +1,7 @@
1
+ export declare class AsyncMutex {
2
+ private queue;
3
+ private head;
4
+ private locked;
5
+ acquire(): Promise<() => void>;
6
+ private createRelease;
7
+ }
@@ -0,0 +1,38 @@
1
+ export class AsyncMutex {
2
+ queue = [];
3
+ head = 0;
4
+ locked = false;
5
+ acquire() {
6
+ if (!this.locked) {
7
+ this.locked = true;
8
+ return Promise.resolve(this.createRelease());
9
+ }
10
+ return new Promise((resolve) => {
11
+ this.queue.push(() => resolve(this.createRelease()));
12
+ });
13
+ }
14
+ createRelease() {
15
+ let released = false;
16
+ return () => {
17
+ if (released)
18
+ return;
19
+ released = true;
20
+ if (this.head < this.queue.length) {
21
+ const next = this.queue[this.head];
22
+ this.queue[this.head] = undefined; // allow GC
23
+ this.head += 1;
24
+ // Compact when more than half the array is dead entries and above threshold
25
+ if (this.head > 1024 && this.head > (this.queue.length >>> 1)) {
26
+ this.queue = this.queue.slice(this.head);
27
+ this.head = 0;
28
+ }
29
+ next();
30
+ }
31
+ else {
32
+ this.queue.length = 0;
33
+ this.head = 0;
34
+ this.locked = false;
35
+ }
36
+ };
37
+ }
38
+ }
@@ -0,0 +1,2 @@
1
+ import type { DatastoreErrorListener } from '../../types.js';
2
+ export declare const emitAutoCommitErrorToListeners: (listeners: Set<DatastoreErrorListener>, error: unknown) => void;
@@ -0,0 +1,22 @@
1
+ import { StorageEngineError } from '../../errors/index.js';
2
+ export const emitAutoCommitErrorToListeners = (listeners, error) => {
3
+ const storageError = error instanceof StorageEngineError
4
+ ? error
5
+ : new StorageEngineError(error instanceof Error
6
+ ? error.message
7
+ : 'Unknown auto-commit storage failure.', { cause: error });
8
+ const event = {
9
+ source: 'autoCommit',
10
+ error: storageError,
11
+ occurredAt: Date.now(),
12
+ };
13
+ for (const listener of listeners) {
14
+ try {
15
+ const delivered = listener(event);
16
+ void Promise.resolve(delivered).catch(() => undefined);
17
+ }
18
+ catch {
19
+ // listener isolation by contract
20
+ }
21
+ }
22
+ };
@@ -0,0 +1,2 @@
1
+ import type { CapacityState } from './types.js';
2
+ export declare const enforceCapacityPolicy: (capacityState: CapacityState | null, currentSizeBytes: number, encodedBytes: number, getRecordCount: () => number, evictOldestRecord: () => number) => number;
@@ -0,0 +1,27 @@
1
+ import { IndexCorruptionError, QuotaExceededError } from '../../errors/index.js';
2
+ export const enforceCapacityPolicy = (capacityState, currentSizeBytes, encodedBytes, getRecordCount, evictOldestRecord) => {
3
+ if (capacityState === null) {
4
+ return currentSizeBytes;
5
+ }
6
+ if (encodedBytes > capacityState.maxSizeBytes) {
7
+ throw new QuotaExceededError('Record exceeds configured capacity.maxSize boundary.');
8
+ }
9
+ if (capacityState.policy === 'strict') {
10
+ if (currentSizeBytes + encodedBytes > capacityState.maxSizeBytes) {
11
+ throw new QuotaExceededError('Insert exceeds configured capacity.maxSize under strict policy.');
12
+ }
13
+ return currentSizeBytes;
14
+ }
15
+ let nextSizeBytes = currentSizeBytes;
16
+ while (nextSizeBytes + encodedBytes > capacityState.maxSizeBytes) {
17
+ if (getRecordCount() === 0) {
18
+ throw new QuotaExceededError('Record cannot fit in turnover policy with empty datastore.');
19
+ }
20
+ const evictedBytes = evictOldestRecord();
21
+ if (!Number.isSafeInteger(evictedBytes) || evictedBytes <= 0) {
22
+ throw new IndexCorruptionError('Turnover eviction reported non-progressing reclaimed bytes.');
23
+ }
24
+ nextSizeBytes -= evictedBytes;
25
+ }
26
+ return Math.max(0, nextSizeBytes);
27
+ };
@@ -0,0 +1,3 @@
1
+ import type { DatastoreConfig } from '../../types.js';
2
+ import type { CapacityState } from './types.js';
3
+ export declare const resolveCapacityState: (config: DatastoreConfig) => CapacityState | null;
@@ -0,0 +1,25 @@
1
+ import { ConfigurationError } from '../../errors/index.js';
2
+ import { parseCapacityConfig } from '../config/config.shared.js';
3
+ const resolveCapacityConfigWithBackendLimit = (config) => {
4
+ if (config.capacity === undefined) {
5
+ return undefined;
6
+ }
7
+ if (config.capacity.maxSize !== 'backendLimit') {
8
+ return config.capacity;
9
+ }
10
+ if (config.driver === undefined) {
11
+ throw new ConfigurationError('capacity.maxSize "backendLimit" requires a durable driver.');
12
+ }
13
+ if (config.driver.resolveBackendLimitBytes === undefined) {
14
+ throw new ConfigurationError('capacity.maxSize "backendLimit" is not supported by the selected driver.');
15
+ }
16
+ const resolvedMaxSize = config.driver.resolveBackendLimitBytes();
17
+ return {
18
+ ...config.capacity,
19
+ maxSize: resolvedMaxSize,
20
+ };
21
+ };
22
+ export const resolveCapacityState = (config) => {
23
+ const resolvedCapacityConfig = resolveCapacityConfigWithBackendLimit(config);
24
+ return parseCapacityConfig(resolvedCapacityConfig);
25
+ };
@@ -0,0 +1,17 @@
1
+ import type { RecordPayload } from '../../types.js';
2
+ export declare const computeUtf8ByteLength: (value: string) => number;
3
+ /**
4
+ * Compute the UTF-8 byte length of a string as it would appear in JSON output
5
+ * (i.e., with JSON escaping applied), INCLUDING the surrounding quotes.
6
+ */
7
+ export declare const estimateJsonStringBytes: (value: string) => number;
8
+ /**
9
+ * Estimate the UTF-8 byte length of `JSON.stringify(value)` by walking the
10
+ * object tree structurally. Does NOT call JSON.stringify.
11
+ *
12
+ * Supports: null, boolean, number, string, plain objects.
13
+ * Arrays are NOT supported (payloads don't contain arrays).
14
+ */
15
+ export declare const estimateObjectSizeBytes: (value: unknown) => number;
16
+ export declare const estimateRecordSizeBytes: (key: unknown, payload: RecordPayload) => number;
17
+ export declare const estimateKeySizeBytes: (key: unknown) => number;
@@ -0,0 +1,148 @@
1
+ // ---------------------------------------------------------------------------
2
+ // P8: Platform-native UTF-8 byte length with JS fallback for browsers
3
+ // ---------------------------------------------------------------------------
4
+ const computeUtf8ByteLengthJs = (value) => {
5
+ let bytes = 0;
6
+ for (let i = 0; i < value.length; i++) {
7
+ const code = value.charCodeAt(i);
8
+ if (code <= 0x7f) {
9
+ bytes += 1;
10
+ }
11
+ else if (code <= 0x7ff) {
12
+ bytes += 2;
13
+ }
14
+ else if (code >= 0xd800 && code <= 0xdbff) {
15
+ const next = i + 1 < value.length ? value.charCodeAt(i + 1) : 0;
16
+ if (next >= 0xdc00 && next <= 0xdfff) {
17
+ bytes += 4;
18
+ i++; // skip low surrogate
19
+ }
20
+ else {
21
+ bytes += 3; // lone high surrogate → U+FFFD replacement (3 bytes)
22
+ }
23
+ }
24
+ else if (code >= 0xdc00 && code <= 0xdfff) {
25
+ bytes += 3; // lone low surrogate → U+FFFD replacement (3 bytes)
26
+ }
27
+ else {
28
+ bytes += 3;
29
+ }
30
+ }
31
+ return bytes;
32
+ };
33
+ const hasBuffer = typeof Buffer !== 'undefined' && typeof Buffer.byteLength === 'function';
34
+ export const computeUtf8ByteLength = hasBuffer
35
+ ? (value) => Buffer.byteLength(value, 'utf8')
36
+ : computeUtf8ByteLengthJs;
37
+ // ---------------------------------------------------------------------------
38
+ // P9: Structural size estimation — walk object tree without JSON.stringify
39
+ // ---------------------------------------------------------------------------
40
+ /**
41
+ * Compute the UTF-8 byte length of a string as it would appear in JSON output
42
+ * (i.e., with JSON escaping applied), INCLUDING the surrounding quotes.
43
+ */
44
+ export const estimateJsonStringBytes = (value) => {
45
+ // 2 bytes for the surrounding quotes
46
+ let bytes = 2;
47
+ for (let i = 0; i < value.length; i++) {
48
+ const code = value.charCodeAt(i);
49
+ // JSON.stringify escapes: " → \", \\ → \\, \n → \\n, \r → \\r, \t → \\t,
50
+ // \b → \\b, \f → \\f, and U+0000–U+001F → \\uXXXX (6 chars)
51
+ if (code === 0x22 || code === 0x5c) {
52
+ // " or \ → 2 ASCII bytes
53
+ bytes += 2;
54
+ }
55
+ else if (code <= 0x1f) {
56
+ // Control characters: \b(8), \t(9), \n(10), \f(12), \r(13) → 2 bytes each
57
+ // Others → \uXXXX → 6 bytes each
58
+ if (code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d) {
59
+ bytes += 2;
60
+ }
61
+ else {
62
+ bytes += 6;
63
+ }
64
+ }
65
+ else if (code <= 0x7f) {
66
+ bytes += 1;
67
+ }
68
+ else if (code <= 0x7ff) {
69
+ bytes += 2;
70
+ }
71
+ else if (code >= 0xd800 && code <= 0xdbff) {
72
+ const next = i + 1 < value.length ? value.charCodeAt(i + 1) : 0;
73
+ if (next >= 0xdc00 && next <= 0xdfff) {
74
+ bytes += 4;
75
+ i++; // skip low surrogate
76
+ }
77
+ else {
78
+ bytes += 6; // lone high surrogate → JSON.stringify emits \uDXXX (6 ASCII bytes)
79
+ }
80
+ }
81
+ else if (code >= 0xdc00 && code <= 0xdfff) {
82
+ bytes += 6; // lone low surrogate → JSON.stringify emits \uDXXX (6 ASCII bytes)
83
+ }
84
+ else {
85
+ bytes += 3;
86
+ }
87
+ }
88
+ return bytes;
89
+ };
90
+ /**
91
+ * Estimate the UTF-8 byte length of `JSON.stringify(value)` by walking the
92
+ * object tree structurally. Does NOT call JSON.stringify.
93
+ *
94
+ * Supports: null, boolean, number, string, plain objects.
95
+ * Arrays are NOT supported (payloads don't contain arrays).
96
+ */
97
+ export const estimateObjectSizeBytes = (value) => {
98
+ if (value === null) {
99
+ return 4; // "null"
100
+ }
101
+ switch (typeof value) {
102
+ case 'boolean':
103
+ return value ? 4 : 5; // "true" / "false"
104
+ case 'number':
105
+ // Use String() for correctness with all number representations
106
+ return String(value).length;
107
+ case 'string':
108
+ return estimateJsonStringBytes(value);
109
+ case 'object': {
110
+ const obj = value;
111
+ let size = 2; // { }
112
+ let visibleCount = 0;
113
+ for (const k of Object.keys(obj)) {
114
+ const v = obj[k];
115
+ // JSON.stringify omits undefined values
116
+ if (v === undefined) {
117
+ continue;
118
+ }
119
+ if (visibleCount > 0) {
120
+ size += 1; // comma
121
+ }
122
+ // key: quoted key string + colon
123
+ size += estimateJsonStringBytes(k) + 1;
124
+ // value
125
+ size += estimateObjectSizeBytes(v);
126
+ visibleCount++;
127
+ }
128
+ return size;
129
+ }
130
+ default:
131
+ return 0;
132
+ }
133
+ };
134
+ // ---------------------------------------------------------------------------
135
+ // P9: Refactored estimateRecordSizeBytes using structural estimation
136
+ // ---------------------------------------------------------------------------
137
+ // JSON_ROOT_WRAPPER_OVERHEAD = 15 matches: [key,{"payload":...}]
138
+ // Breakdown: [ + key + , + {"payload": + payload_json + } + ] = 1+1+10+1+1+1 = 15 overhead
139
+ const JSON_ROOT_WRAPPER_OVERHEAD = 15;
140
+ export const estimateRecordSizeBytes = (key, payload) => {
141
+ return estimateObjectSizeBytes(key) + estimateObjectSizeBytes(payload) + JSON_ROOT_WRAPPER_OVERHEAD;
142
+ };
143
+ // ---------------------------------------------------------------------------
144
+ // P10: estimateKeySizeBytes
145
+ // ---------------------------------------------------------------------------
146
+ export const estimateKeySizeBytes = (key) => {
147
+ return estimateObjectSizeBytes(key);
148
+ };