web-dc-api 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.js +1 -1
- package/dist/dc.min.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/index.d.ts +934 -878
- package/package.json +4 -8
- package/dist/cjs/helia-core-B1Xqha7a.js +0 -1
- package/dist/cjs/helia-core-D8Uv1KjQ.js +0 -1
- package/dist/cjs/polkadot-api-7PhQf3ws.js +0 -1
- package/dist/cjs/polkadot-api-CtrJVWuZ.js +0 -1
- package/dist/esm/chunks/helia-core-BxMqyK2Y.js +0 -1
- package/dist/esm/chunks/helia-core-DMXRpcO-.js +0 -1
- package/dist/esm/chunks/polkadot-api-5Y9Bw8VT.js +0 -1
- package/dist/esm/chunks/polkadot-api-D69Ioun_.js +0 -1
- package/lib/common/blowfish/block.ts +0 -259
- package/lib/common/blowfish/cipher.ts +0 -144
- package/lib/common/blowfish/const.ts +0 -195
- package/lib/common/chain.ts +0 -469
- package/lib/common/commonclient.ts +0 -202
- package/lib/common/constants.ts +0 -55
- package/lib/common/dc-key/ed25519.ts +0 -343
- package/lib/common/dc-key/keyManager.ts +0 -424
- package/lib/common/dcapi.ts +0 -98
- package/lib/common/dcutil.ts +0 -627
- package/lib/common/define.ts +0 -70
- package/lib/common/error.ts +0 -67
- package/lib/common/grpc-dc.ts +0 -104
- package/lib/common/module-system.ts +0 -184
- package/lib/common/service-worker.ts +0 -234
- package/lib/common/types/types.ts +0 -344
- package/lib/dc.ts +0 -701
- package/lib/implements/account/client.ts +0 -185
- package/lib/implements/account/manager.ts +0 -683
- package/lib/implements/aiproxy/client.ts +0 -357
- package/lib/implements/aiproxy/manager.ts +0 -670
- package/lib/implements/cache/client.ts +0 -105
- package/lib/implements/cache/manager.ts +0 -127
- package/lib/implements/comment/client.ts +0 -982
- package/lib/implements/comment/manager.ts +0 -1151
- package/lib/implements/dc/client.ts +0 -51
- package/lib/implements/dc/manager.ts +0 -33
- package/lib/implements/file/client.ts +0 -253
- package/lib/implements/file/file-cache-manager.ts +0 -142
- package/lib/implements/file/manager.ts +0 -1240
- package/lib/implements/file/seekableFileStream.ts +0 -344
- package/lib/implements/file/streamwriter.ts +0 -322
- package/lib/implements/keyvalue/client.ts +0 -376
- package/lib/implements/keyvalue/manager.ts +0 -759
- package/lib/implements/message/client.ts +0 -250
- package/lib/implements/message/manager.ts +0 -215
- package/lib/implements/threaddb/cbor/coding.ts +0 -62
- package/lib/implements/threaddb/cbor/event.ts +0 -336
- package/lib/implements/threaddb/cbor/node.ts +0 -542
- package/lib/implements/threaddb/cbor/record.ts +0 -398
- package/lib/implements/threaddb/common/AsyncMutex.ts +0 -24
- package/lib/implements/threaddb/common/addrinfo.ts +0 -135
- package/lib/implements/threaddb/common/dispatcher.ts +0 -81
- package/lib/implements/threaddb/common/idbstore-adapter.ts +0 -260
- package/lib/implements/threaddb/common/json-patcher.ts +0 -204
- package/lib/implements/threaddb/common/key.ts +0 -290
- package/lib/implements/threaddb/common/level-adapter.ts +0 -235
- package/lib/implements/threaddb/common/lineReader.ts +0 -79
- package/lib/implements/threaddb/common/logstore.ts +0 -215
- package/lib/implements/threaddb/common/transformed-datastore.ts +0 -308
- package/lib/implements/threaddb/core/app.ts +0 -206
- package/lib/implements/threaddb/core/core.ts +0 -230
- package/lib/implements/threaddb/core/db.ts +0 -249
- package/lib/implements/threaddb/core/event.ts +0 -54
- package/lib/implements/threaddb/core/head.ts +0 -89
- package/lib/implements/threaddb/core/identity.ts +0 -171
- package/lib/implements/threaddb/core/logstore.ts +0 -137
- package/lib/implements/threaddb/core/options.ts +0 -14
- package/lib/implements/threaddb/core/record.ts +0 -54
- package/lib/implements/threaddb/db/collection.ts +0 -1910
- package/lib/implements/threaddb/db/db.ts +0 -698
- package/lib/implements/threaddb/db/json2Query.ts +0 -192
- package/lib/implements/threaddb/db/query.ts +0 -524
- package/lib/implements/threaddb/dbclient.ts +0 -543
- package/lib/implements/threaddb/dbmanager.ts +0 -1906
- package/lib/implements/threaddb/lsstoreds/addr_book.ts +0 -549
- package/lib/implements/threaddb/lsstoreds/cache.ts +0 -36
- package/lib/implements/threaddb/lsstoreds/cyclic_batch.ts +0 -87
- package/lib/implements/threaddb/lsstoreds/global.ts +0 -151
- package/lib/implements/threaddb/lsstoreds/headbook.ts +0 -373
- package/lib/implements/threaddb/lsstoreds/keybook.ts +0 -297
- package/lib/implements/threaddb/lsstoreds/logstore.ts +0 -29
- package/lib/implements/threaddb/lsstoreds/metadata.ts +0 -223
- package/lib/implements/threaddb/net/define.ts +0 -149
- package/lib/implements/threaddb/net/grpcClient.ts +0 -589
- package/lib/implements/threaddb/net/grpcserver.ts +0 -146
- package/lib/implements/threaddb/net/net.ts +0 -2047
- package/lib/implements/threaddb/pb/lstore.proto +0 -38
- package/lib/implements/threaddb/pb/lstore.ts +0 -393
- package/lib/implements/threaddb/pb/lstore_pb.d.ts +0 -433
- package/lib/implements/threaddb/pb/lstore_pb.js +0 -1085
- package/lib/implements/threaddb/pb/net.proto +0 -194
- package/lib/implements/threaddb/pb/net_pb.d.ts +0 -2349
- package/lib/implements/threaddb/pb/net_pb.js +0 -5525
- package/lib/implements/threaddb/pb/proto-custom-types.ts +0 -212
- package/lib/implements/util/client.ts +0 -72
- package/lib/implements/util/manager.ts +0 -146
- package/lib/implements/wallet/manager.ts +0 -671
- package/lib/index.ts +0 -57
- package/lib/interfaces/DCContext.ts +0 -51
- package/lib/interfaces/aiproxy-interface.ts +0 -145
- package/lib/interfaces/auth-interface.ts +0 -118
- package/lib/interfaces/cache-interface.ts +0 -22
- package/lib/interfaces/client-interface.ts +0 -11
- package/lib/interfaces/comment-interface.ts +0 -167
- package/lib/interfaces/components/news-component.ts +0 -0
- package/lib/interfaces/database-interface.ts +0 -169
- package/lib/interfaces/file-interface.ts +0 -120
- package/lib/interfaces/index.ts +0 -10
- package/lib/interfaces/keyvalue-interface.ts +0 -156
- package/lib/interfaces/message-interface.ts +0 -22
- package/lib/interfaces/util-interface.ts +0 -31
- package/lib/modules/aiproxy-module.ts +0 -246
- package/lib/modules/auth-module.ts +0 -753
- package/lib/modules/cache-module.ts +0 -99
- package/lib/modules/client-module.ts +0 -71
- package/lib/modules/comment-module.ts +0 -429
- package/lib/modules/components/news-components.ts +0 -390
- package/lib/modules/database-module.ts +0 -598
- package/lib/modules/file-module.ts +0 -291
- package/lib/modules/index.ts +0 -13
- package/lib/modules/keyvalue-module.ts +0 -379
- package/lib/modules/message-module.ts +0 -107
- package/lib/modules/util-module.ts +0 -148
- package/lib/polyfills/process-env-browser.ts +0 -1
- package/lib/proto/datasource.ts +0 -93
- package/lib/proto/dcnet.proto +0 -1601
- package/lib/proto/dcnet_proto.d.ts +0 -22857
- package/lib/proto/dcnet_proto.js +0 -55204
- package/lib/proto/dcnet_proto_sparse.js +0 -55166
- package/lib/proto/oidfetch.proto +0 -25
- package/lib/proto/oidfetch_proto.d.ts +0 -585
- package/lib/proto/oidfetch_proto.js +0 -1247
- package/lib/serverless/babel-browser.ts +0 -39
- package/lib/serverless/base_entity.ts +0 -78
- package/lib/serverless/base_repository.ts +0 -414
- package/lib/serverless/browser_schema_extractor.ts +0 -283
- package/lib/serverless/decorator_factory.ts +0 -322
- package/lib/util/BrowserLineReader.ts +0 -73
- package/lib/util/base64.ts +0 -105
- package/lib/util/bcrypt.ts +0 -206
- package/lib/util/curve25519Encryption.ts +0 -418
- package/lib/util/dccrypt.ts +0 -73
- package/lib/util/logger.ts +0 -104
- package/lib/util/utils.ts +0 -289
|
@@ -1,1910 +0,0 @@
|
|
|
1
|
-
import { Key,QueryFilter } from 'interface-datastore';
|
|
2
|
-
import {AnySchema} from 'ajv';
|
|
3
|
-
import Ajv from 'ajv';
|
|
4
|
-
import { nanoid } from 'nanoid';
|
|
5
|
-
import EventEmitter from 'eventemitter3';
|
|
6
|
-
|
|
7
|
-
import { Ed25519PrivKey as PrivKey,Ed25519PubKey as PubKey} from "../../../common/dc-key/ed25519";
|
|
8
|
-
import { Action, CoreActionType, Event,ITxn ,idFieldName} from '../core/db';
|
|
9
|
-
import { ThreadID } from '@textile/threads-id';
|
|
10
|
-
import { IPLDNode } from '../core/core';
|
|
11
|
-
import { ICollectionConfig } from '../core/core';
|
|
12
|
-
import {dsPrefix,IDB,ICollection,DBPrefix} from '../core/db';
|
|
13
|
-
import {ThreadToken} from '../core/identity';
|
|
14
|
-
import {dsDispatcherPrefix} from '../common/dispatcher';
|
|
15
|
-
import {Query,compare,traverseFieldPathMap} from './query';
|
|
16
|
-
import * as cbornode from '../cbor/node';
|
|
17
|
-
import * as dagCBOR from '@ipld/dag-cbor';
|
|
18
|
-
import { dagCbor } from '@helia/dag-cbor';
|
|
19
|
-
import { jsonStringify } from '../../../util/utils';
|
|
20
|
-
|
|
21
|
-
// iteratorKeyMinCacheSize is the size of iterator keys stored in memory before more are fetched.
|
|
22
|
-
const iteratorKeyMinCacheSize = 100
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// Error classes for index operations
|
|
26
|
-
class IndexError extends Error {
|
|
27
|
-
constructor(message: string) {
|
|
28
|
-
super(message);
|
|
29
|
-
this.name = 'IndexError';
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Error constants
|
|
34
|
-
const ErrUniqueExists = new IndexError("unique constraint violation");
|
|
35
|
-
const ErrNotIndexable = new IndexError("value not indexable");
|
|
36
|
-
const ErrCantCreateUniqueIndex = new IndexError("can't create unique index (duplicate instances exist)");
|
|
37
|
-
const ErrIndexNotFound = new IndexError("index not found");
|
|
38
|
-
|
|
39
|
-
// Index key prefix
|
|
40
|
-
const indexPrefix = new Key("_index");
|
|
41
|
-
|
|
42
|
-
// Valid index types
|
|
43
|
-
const indexTypes: string[] = ["string", "number", "integer", "boolean"];
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Index defines an index configuration
|
|
47
|
-
*/
|
|
48
|
-
export interface Index {
|
|
49
|
-
/**
|
|
50
|
-
* Path to the field to index in dot syntax, e.g., "name.last" or "age".
|
|
51
|
-
*/
|
|
52
|
-
path: string;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Unique indicates that only one instance should exist per field value.
|
|
56
|
-
*/
|
|
57
|
-
unique?: boolean;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* MarshaledResult 表示查询结果
|
|
62
|
-
*/
|
|
63
|
-
interface MarshaledResult {
|
|
64
|
-
instanceID: InstanceID;
|
|
65
|
-
key: string;
|
|
66
|
-
value: Uint8Array;
|
|
67
|
-
marshaledValue?: Record<string, any>;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Error types
|
|
73
|
-
*/
|
|
74
|
-
export class InvalidSortingFieldError extends Error {
|
|
75
|
-
constructor() {
|
|
76
|
-
super("sorting field doesn't correspond to instance type");
|
|
77
|
-
this.name = "InvalidSortingFieldError";
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// 常量定义
|
|
82
|
-
|
|
83
|
-
const ErrInvalidSortingField = new InvalidSortingFieldError();
|
|
84
|
-
// Error definitions
|
|
85
|
-
class ThreadDBError extends Error {
|
|
86
|
-
constructor(message: string) {
|
|
87
|
-
super(message);
|
|
88
|
-
this.name = 'ThreadDBError';
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Define specific error types
|
|
93
|
-
const ErrInvalidCollectionSchemaPath = new ThreadDBError('collection schema does not contain path');
|
|
94
|
-
const ErrCollectionNotFound = new ThreadDBError('collection not found');
|
|
95
|
-
const ErrCollectionAlreadyRegistered = new ThreadDBError('collection already registered');
|
|
96
|
-
const ErrInstanceNotFound = new ThreadDBError('instance not found');
|
|
97
|
-
const ErrReadonlyTx = new ThreadDBError('read only transaction');
|
|
98
|
-
const ErrInvalidSchemaInstance = new ThreadDBError('instance doesn\'t correspond to schema');
|
|
99
|
-
const ErrInvalidCollectionSchema = new ThreadDBError('invalid collection schema');
|
|
100
|
-
const ErrInvalidName = new ThreadDBError('invalid collection name');
|
|
101
|
-
|
|
102
|
-
const errMissingInstanceID = new ThreadDBError('invalid instance: missing _id attribute');
|
|
103
|
-
const errAlreadyDiscardedCommitedTxn = new ThreadDBError('can\'t commit discarded/committed txn');
|
|
104
|
-
const errCantCreateExistingInstance = new ThreadDBError('can\'t create already existing instance');
|
|
105
|
-
|
|
106
|
-
// Constants
|
|
107
|
-
const baseKey = dsPrefix.child(new Key('collection'));
|
|
108
|
-
const vmTimeout = 200; // milliseconds
|
|
109
|
-
const writeValidatorFn = "_validate";
|
|
110
|
-
const readFilterFn = "_filter";
|
|
111
|
-
|
|
112
|
-
const createNetRecordTimeout = 30000; // 30 seconds
|
|
113
|
-
|
|
114
|
-
// RegExp for validating collection names
|
|
115
|
-
const nameRx = /^[a-zA-Z0-9_]+$/;
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Instance ID type
|
|
119
|
-
*/
|
|
120
|
-
export type InstanceID = string;
|
|
121
|
-
|
|
122
|
-
// Empty instance ID constant
|
|
123
|
-
const EmptyInstanceID = '';
|
|
124
|
-
|
|
125
|
-
// Generate a new instance ID
|
|
126
|
-
function NewInstanceID(): InstanceID {
|
|
127
|
-
return nanoid();
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Collection configuration interface
|
|
132
|
-
*/
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Index definition for collections
|
|
137
|
-
*/
|
|
138
|
-
export interface Index {
|
|
139
|
-
path: string;
|
|
140
|
-
unique?: boolean;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* JavaScript runtime interface for validators
|
|
149
|
-
*/
|
|
150
|
-
interface JSRuntime {
|
|
151
|
-
runScript(script: string): any;
|
|
152
|
-
getGlobal(name: string): any;
|
|
153
|
-
call(fn: string, ...args: any[]): any;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Browser-compatible JS runtime
|
|
158
|
-
*/
|
|
159
|
-
class BrowserJSRuntime implements JSRuntime {
|
|
160
|
-
private context: Record<string, any> = {};
|
|
161
|
-
private functions: Record<string, Function> = {};
|
|
162
|
-
private timeoutId: number | null = null;
|
|
163
|
-
|
|
164
|
-
constructor(private timeout: number = vmTimeout) {}
|
|
165
|
-
|
|
166
|
-
runScript(script: string): any {
|
|
167
|
-
try {
|
|
168
|
-
// Create a function that executes in our context
|
|
169
|
-
const fn = new Function('context', `
|
|
170
|
-
with(context) {
|
|
171
|
-
${script};
|
|
172
|
-
return context;
|
|
173
|
-
}
|
|
174
|
-
`);
|
|
175
|
-
|
|
176
|
-
// Set timeout for long-running scripts
|
|
177
|
-
let completed = false;
|
|
178
|
-
this.timeoutId = window.setTimeout(() => {
|
|
179
|
-
if (!completed) {
|
|
180
|
-
console.warn("JavaScript execution timed out");
|
|
181
|
-
}
|
|
182
|
-
}, this.timeout);
|
|
183
|
-
|
|
184
|
-
// Execute the script
|
|
185
|
-
this.context = fn(this.context) || this.context;
|
|
186
|
-
|
|
187
|
-
// Clear timeout
|
|
188
|
-
completed = true;
|
|
189
|
-
if (this.timeoutId) {
|
|
190
|
-
clearTimeout(this.timeoutId);
|
|
191
|
-
this.timeoutId = null;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return true;
|
|
195
|
-
} catch (err) {
|
|
196
|
-
if (this.timeoutId) {
|
|
197
|
-
clearTimeout(this.timeoutId);
|
|
198
|
-
this.timeoutId = null;
|
|
199
|
-
}
|
|
200
|
-
throw new Error(`Script execution error: ${err instanceof Error ? err.message : String(err)}`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
getGlobal(name: string): any {
|
|
205
|
-
return this.context[name];
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
call(fn: string, ...args: any[]): any {
|
|
209
|
-
const func = this.context[fn];
|
|
210
|
-
if (typeof func !== 'function') {
|
|
211
|
-
throw new Error(`${fn} is not a function`);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
// Set timeout for long-running functions
|
|
216
|
-
let completed = false;
|
|
217
|
-
this.timeoutId = window.setTimeout(() => {
|
|
218
|
-
if (!completed) {
|
|
219
|
-
console.warn("JavaScript function call timed out");
|
|
220
|
-
}
|
|
221
|
-
}, this.timeout);
|
|
222
|
-
|
|
223
|
-
// Execute the function
|
|
224
|
-
const result = func.apply(this.context, args);
|
|
225
|
-
|
|
226
|
-
// Clear timeout
|
|
227
|
-
completed = true;
|
|
228
|
-
if (this.timeoutId) {
|
|
229
|
-
clearTimeout(this.timeoutId);
|
|
230
|
-
this.timeoutId = null;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return result;
|
|
234
|
-
} catch (err) {
|
|
235
|
-
if (this.timeoutId) {
|
|
236
|
-
clearTimeout(this.timeoutId);
|
|
237
|
-
this.timeoutId = null;
|
|
238
|
-
}
|
|
239
|
-
throw new Error(`Function execution error: ${err instanceof Error ? err.message : String(err)}`);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Connector interface for DB
|
|
246
|
-
*/
|
|
247
|
-
interface Connector {
|
|
248
|
-
validate(token: ThreadToken, readOnly: boolean): Promise<Error | null>;
|
|
249
|
-
createNetRecord(node: IPLDNode, token: ThreadToken): Promise<any>;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Collection class - a group of instances sharing a schema
|
|
256
|
-
*/
|
|
257
|
-
export class Collection implements ICollection {
|
|
258
|
-
private schemaValidator: Ajv;
|
|
259
|
-
private vm: JSRuntime;
|
|
260
|
-
private writeValidator: ((writer: any, event: any, instance: any) => boolean) | null = null;
|
|
261
|
-
public readFilter: ((reader: any, instance: any) => any) | null = null;
|
|
262
|
-
public indexes: Map<string, Index> = new Map();
|
|
263
|
-
|
|
264
|
-
constructor(
|
|
265
|
-
public readonly name: string,
|
|
266
|
-
public schema: AnySchema,
|
|
267
|
-
public db: IDB,
|
|
268
|
-
public rawWriteValidator?: string,
|
|
269
|
-
public rawReadFilter?: string
|
|
270
|
-
) {
|
|
271
|
-
// Initialize schema validator
|
|
272
|
-
this.schemaValidator = new Ajv({ allErrors: true });
|
|
273
|
-
this.vm = new BrowserJSRuntime(vmTimeout);
|
|
274
|
-
|
|
275
|
-
// Compile and load validators if provided
|
|
276
|
-
if (rawWriteValidator) {
|
|
277
|
-
this.compileAndLoadValidator(rawWriteValidator, writeValidatorFn, ['writer', 'event', 'instance']);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (rawReadFilter) {
|
|
281
|
-
this.compileAndLoadValidator(rawReadFilter, readFilterFn, ['reader', 'instance']);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
private compileAndLoadValidator(source: string, name: string, args: string[]): void {
|
|
286
|
-
try {
|
|
287
|
-
const script = `function ${name}(${args.join(",")}) {${source}}`;
|
|
288
|
-
this.vm.runScript(script);
|
|
289
|
-
|
|
290
|
-
// Test the function exists
|
|
291
|
-
const fn = this.vm.getGlobal(name);
|
|
292
|
-
if (typeof fn !== 'function') {
|
|
293
|
-
throw new Error(`${name} is not a function`);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Set the appropriate validator
|
|
297
|
-
if (name === writeValidatorFn) {
|
|
298
|
-
this.writeValidator = (writer: any, event: any, instance: any) =>
|
|
299
|
-
this.vm.call(writeValidatorFn, writer, event, instance);
|
|
300
|
-
} else if (name === readFilterFn) {
|
|
301
|
-
this.readFilter = (reader: any, instance: any) =>
|
|
302
|
-
this.vm.call(readFilterFn, reader, instance);
|
|
303
|
-
}
|
|
304
|
-
} catch (err) {
|
|
305
|
-
throw new Error(`Failed to compile ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Get base key for this collection
|
|
311
|
-
*/
|
|
312
|
-
baseKey(): Key {
|
|
313
|
-
return baseKey.child(new Key(this.name));
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Get collection name
|
|
318
|
-
*/
|
|
319
|
-
getName(): string {
|
|
320
|
-
return this.name;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Get collection schema
|
|
325
|
-
*/
|
|
326
|
-
getSchema(): Uint8Array {
|
|
327
|
-
return new TextEncoder().encode(JSON.stringify(this.schema));
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Get write validator
|
|
332
|
-
*/
|
|
333
|
-
getWriteValidator(): Uint8Array {
|
|
334
|
-
return new TextEncoder().encode(this.rawWriteValidator || '');
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Get read filter
|
|
339
|
-
*/
|
|
340
|
-
getReadFilter(): Uint8Array {
|
|
341
|
-
return new TextEncoder().encode(this.rawReadFilter || '');
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Create a read transaction
|
|
346
|
-
*/
|
|
347
|
-
async readTxn(fn: (txn: ITxn) => Promise<void> , token?: ThreadToken): Promise<void> {
|
|
348
|
-
return this.db.readTxn(this, fn, token);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Create a write transaction
|
|
353
|
-
*/
|
|
354
|
-
async writeTxn(fn: (txn: ITxn) => Promise<void> , token?: ThreadToken): Promise<void> {
|
|
355
|
-
return this.db.writeTxn(this, fn, token);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Find instance by ID
|
|
360
|
-
*/
|
|
361
|
-
async findByID(id: InstanceID, token?: ThreadToken): Promise<Object> {
|
|
362
|
-
let instance: Object | null = null;
|
|
363
|
-
|
|
364
|
-
await this.readTxn(async (txn) => {
|
|
365
|
-
instance = await txn.findByID(id);
|
|
366
|
-
}, token);
|
|
367
|
-
|
|
368
|
-
return instance!;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Create a new instance
|
|
373
|
-
*/
|
|
374
|
-
async create(v: Uint8Array, token?: ThreadToken): Promise<InstanceID> {
|
|
375
|
-
let id: InstanceID = '';
|
|
376
|
-
|
|
377
|
-
await this.writeTxn(async (txn) => {
|
|
378
|
-
const ids = await txn.create(v);
|
|
379
|
-
if (ids.length > 0) {
|
|
380
|
-
id = ids[0] as InstanceID;
|
|
381
|
-
}
|
|
382
|
-
}, token);
|
|
383
|
-
|
|
384
|
-
return id;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Create multiple instances
|
|
389
|
-
*/
|
|
390
|
-
async createMany(vs: Uint8Array[], token?: ThreadToken): Promise<InstanceID[]> {
|
|
391
|
-
let ids: InstanceID[] = [];
|
|
392
|
-
|
|
393
|
-
await this.writeTxn(async (txn) => {
|
|
394
|
-
ids = await txn.create(...vs);
|
|
395
|
-
}, token);
|
|
396
|
-
|
|
397
|
-
return ids;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Delete an instance by ID
|
|
402
|
-
*/
|
|
403
|
-
async delete(id: InstanceID, token?: ThreadToken): Promise<void> {
|
|
404
|
-
await this.writeTxn(async (txn) => {
|
|
405
|
-
await txn.delete(id);
|
|
406
|
-
}, token);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Delete multiple instances by ID
|
|
411
|
-
*/
|
|
412
|
-
async deleteMany(ids: InstanceID[], token?: ThreadToken): Promise<void> {
|
|
413
|
-
await this.writeTxn(async (txn) => {
|
|
414
|
-
await txn.delete(...ids);
|
|
415
|
-
}, token);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* Save changes to an instance
|
|
420
|
-
*/
|
|
421
|
-
async save(v: Uint8Array, token?: ThreadToken): Promise<void> {
|
|
422
|
-
await this.writeTxn(async (txn) => {
|
|
423
|
-
await txn.save(v);
|
|
424
|
-
}, token);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* Save changes to multiple instances
|
|
429
|
-
*/
|
|
430
|
-
async saveMany(vs: Uint8Array[], token?: ThreadToken): Promise<void> {
|
|
431
|
-
await this.writeTxn(async (txn) => {
|
|
432
|
-
await txn.save(...vs);
|
|
433
|
-
}, token);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Verify changes to an instance
|
|
438
|
-
*/
|
|
439
|
-
async verify(v: Uint8Array, token?: ThreadToken): Promise<void> {
|
|
440
|
-
await this.writeTxn(async (txn) => {
|
|
441
|
-
await txn.verify(v);
|
|
442
|
-
}, token);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Verify changes to multiple instances
|
|
447
|
-
*/
|
|
448
|
-
async verifyMany(vs: Uint8Array[], token?: ThreadToken): Promise<void> {
|
|
449
|
-
await this.writeTxn(async (txn) => {
|
|
450
|
-
await txn.verify(...vs);
|
|
451
|
-
}, token);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* Check if an instance exists
|
|
456
|
-
*/
|
|
457
|
-
async has(id: InstanceID, token?: ThreadToken): Promise<boolean> {
|
|
458
|
-
let exists = false;
|
|
459
|
-
|
|
460
|
-
await this.readTxn(async (txn) => {
|
|
461
|
-
exists = await txn.has(id);
|
|
462
|
-
}, token);
|
|
463
|
-
|
|
464
|
-
return exists;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* Check if multiple instances exist
|
|
469
|
-
*/
|
|
470
|
-
async hasMany(ids: InstanceID[], token?: ThreadToken): Promise<boolean> {
|
|
471
|
-
let exists = false;
|
|
472
|
-
|
|
473
|
-
await this.readTxn(async (txn) => {
|
|
474
|
-
exists = await txn.has(...ids);
|
|
475
|
-
}, token);
|
|
476
|
-
|
|
477
|
-
return exists;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* Find instances matching a query
|
|
482
|
-
*/
|
|
483
|
-
async find(q: Query, token?: ThreadToken): Promise<Object[]> {
|
|
484
|
-
let instances: Object[] = [];
|
|
485
|
-
|
|
486
|
-
await this.readTxn(async (txn) => {
|
|
487
|
-
instances = await txn.find(q);
|
|
488
|
-
}, token);
|
|
489
|
-
|
|
490
|
-
return instances;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Get instances modified since a specific time
|
|
495
|
-
*/
|
|
496
|
-
async modifiedSince(time: number, token?: ThreadToken): Promise<InstanceID[]> {
|
|
497
|
-
let ids: InstanceID[] = [];
|
|
498
|
-
|
|
499
|
-
await this.readTxn(async (txn) => {
|
|
500
|
-
ids = await txn.modifiedSince(time);
|
|
501
|
-
}, token);
|
|
502
|
-
|
|
503
|
-
return ids;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* Validate an instance against the collection schema
|
|
508
|
-
*/
|
|
509
|
-
validInstance(v: Uint8Array): void {
|
|
510
|
-
try {
|
|
511
|
-
const instance = JSON.parse(new TextDecoder().decode(v));
|
|
512
|
-
const validate = this.schemaValidator.compile(this.schema);
|
|
513
|
-
const valid = validate(instance);
|
|
514
|
-
|
|
515
|
-
if (!valid && validate.errors && validate.errors.length > 0) {
|
|
516
|
-
let msg = '';
|
|
517
|
-
for (let i = 0; i < validate.errors.length; i++) {
|
|
518
|
-
const e = validate.errors[i];
|
|
519
|
-
msg += `${e?.schemaPath}: ${e?.message}`;
|
|
520
|
-
if (i !== validate.errors.length - 1) {
|
|
521
|
-
msg += '; ';
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
throw new Error(`${ErrInvalidSchemaInstance.message}: ${msg}`);
|
|
525
|
-
}
|
|
526
|
-
} catch (err) {
|
|
527
|
-
if (err instanceof Error && err.message.includes(ErrInvalidSchemaInstance.message)) {
|
|
528
|
-
throw err;
|
|
529
|
-
}
|
|
530
|
-
throw new Error(`Error validating instance: ${err instanceof Error ? err.message : String(err)}`);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Validate write operations against the write validator
|
|
536
|
-
*/
|
|
537
|
-
async validWrite(identity: PubKey, e: Event): Promise<void> {
|
|
538
|
-
if (!this.writeValidator) {
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
try {
|
|
543
|
-
// Load identity
|
|
544
|
-
const writer = identity ? identity.toString() : null;
|
|
545
|
-
|
|
546
|
-
// Marshal event
|
|
547
|
-
const data = await e.marshal();
|
|
548
|
-
const eventData = JSON.parse(new TextDecoder().decode(data));
|
|
549
|
-
|
|
550
|
-
// Get instance
|
|
551
|
-
let instanceData = null;
|
|
552
|
-
try {
|
|
553
|
-
const key = this.baseKey().child(new Key(e.instanceID));
|
|
554
|
-
const val = await this.db.datastore.get(key);
|
|
555
|
-
if (val) {
|
|
556
|
-
instanceData = JSON.parse(new TextDecoder().decode(val));
|
|
557
|
-
}
|
|
558
|
-
} catch (err: any) {
|
|
559
|
-
// Only ignore "not found" errors
|
|
560
|
-
if (err.code !== 'ERR_NOT_FOUND') {
|
|
561
|
-
throw err;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// Run validator
|
|
566
|
-
const result = this.writeValidator(writer, eventData, instanceData);
|
|
567
|
-
|
|
568
|
-
if (result !== true) {
|
|
569
|
-
throw new Error('Write validation failed');
|
|
570
|
-
}
|
|
571
|
-
} catch (err) {
|
|
572
|
-
throw new Error(`Write validation error: ${err instanceof Error ? err.message : String(err)}`);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
/**
|
|
577
|
-
* Filter read operations based on the read filter
|
|
578
|
-
*/
|
|
579
|
-
async filterRead(identity: PubKey|undefined, instance: Uint8Array): Promise<Uint8Array | null> {
|
|
580
|
-
if (!this.readFilter) {
|
|
581
|
-
return instance;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
try {
|
|
585
|
-
// Load identity
|
|
586
|
-
const reader = identity ? identity.toString() : null;
|
|
587
|
-
|
|
588
|
-
// Parse instance
|
|
589
|
-
const instanceData = JSON.parse(new TextDecoder().decode(instance));
|
|
590
|
-
|
|
591
|
-
// Run filter
|
|
592
|
-
const result = this.readFilter(reader, instanceData);
|
|
593
|
-
|
|
594
|
-
if (result === null) {
|
|
595
|
-
return null;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
return new TextEncoder().encode(jsonStringify(result));
|
|
599
|
-
} catch (err) {
|
|
600
|
-
throw new Error(`Read filter error: ${err instanceof Error ? err.message : String(err)}`);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
/**
|
|
606
|
-
* 返回当前索引列表
|
|
607
|
-
*/
|
|
608
|
-
getIndexes(): Index[] {
|
|
609
|
-
if (this.indexes.size === 0) {
|
|
610
|
-
return [];
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const indexes: Index[] = [];
|
|
614
|
-
this.indexes.forEach((index, path) => {
|
|
615
|
-
if (path !== idFieldName) {
|
|
616
|
-
indexes.push(index);
|
|
617
|
-
}
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
return indexes;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* 创建基于路径的新索引
|
|
625
|
-
* 使用点语法访问嵌套字段,例如 "name.last"
|
|
626
|
-
* 路径上的字段必须是支持的JSON Schema类型之一: string, number, integer, 或 boolean
|
|
627
|
-
* 设置unique为true可以在该路径上创建唯一性约束
|
|
628
|
-
* 添加索引将覆盖任何已存在的重叠索引值
|
|
629
|
-
* 注意: 这目前不会构建索引。如果在添加新索引之前已添加了项目,它们不会被事后索引。
|
|
630
|
-
*/
|
|
631
|
-
async addIndex( index: Index, token?: ThreadToken): Promise<void> {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
// 不允许覆盖默认索引
|
|
635
|
-
if (index.path === idFieldName) {
|
|
636
|
-
if (this.indexes.has(idFieldName)) {
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// 验证路径和类型
|
|
642
|
-
try {
|
|
643
|
-
const jt = getSchemaTypeAtPath(this.schema, index.path);
|
|
644
|
-
let valid = false;
|
|
645
|
-
for (const t of indexTypes) {
|
|
646
|
-
if (jt?.type === t) {
|
|
647
|
-
valid = true;
|
|
648
|
-
break;
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
if (!valid) {
|
|
652
|
-
throw ErrNotIndexable;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
// 如果没有变化则跳过
|
|
656
|
-
const existingIndex = this.indexes.get(index.path);
|
|
657
|
-
if (existingIndex && existingIndex.unique === index.unique) {
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// 确保集合在该路径上不包含具有相同值的多个实例
|
|
662
|
-
if (index.unique && index.path !== idFieldName) {
|
|
663
|
-
const vals = new Map<any, boolean>();
|
|
664
|
-
const all = await this.find(new Query(), token);
|
|
665
|
-
|
|
666
|
-
for (const item of all) {
|
|
667
|
-
const value = traverseFieldPathMap(item, index.path);
|
|
668
|
-
|
|
669
|
-
if (value === undefined) {
|
|
670
|
-
continue;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
if (vals.has(value)) {
|
|
674
|
-
throw ErrCantCreateUniqueIndex;
|
|
675
|
-
} else {
|
|
676
|
-
vals.set(value, true);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// 保存索引
|
|
682
|
-
this.indexes.set(index.path, index);
|
|
683
|
-
await this.saveIndexes();
|
|
684
|
-
} catch (err) {
|
|
685
|
-
throw err;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
/**
|
|
690
|
-
* 删除指定路径的索引
|
|
691
|
-
*/
|
|
692
|
-
async dropIndex(path: string): Promise<void> {
|
|
693
|
-
// 不允许删除默认索引
|
|
694
|
-
if (path === idFieldName) {
|
|
695
|
-
throw new Error(`${idFieldName} index cannot be dropped`);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
this.indexes.delete(path);
|
|
699
|
-
await this.saveIndexes();
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* 持久化当前索引
|
|
704
|
-
*/
|
|
705
|
-
async saveIndexes(): Promise<void> {
|
|
706
|
-
try {
|
|
707
|
-
const indexesObj: Record<string, Index> = {};
|
|
708
|
-
this.indexes.forEach((index, path) => {
|
|
709
|
-
indexesObj[path] = index;
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
const indexBytes = new TextEncoder().encode(JSON.stringify(indexesObj));
|
|
713
|
-
await this.db.datastore.put(DBPrefix.dsIndexes.child(new Key(this.name)), indexBytes);
|
|
714
|
-
} catch (err) {
|
|
715
|
-
throw new Error(`Failed to save indexes: ${err instanceof Error ? err.message : String(err)}`);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
/**
|
|
720
|
-
* 向索引添加条目
|
|
721
|
-
*/
|
|
722
|
-
async indexAdd(txn: any, key: Key, data: Uint8Array): Promise<void> {
|
|
723
|
-
for (const [path, index] of this.indexes.entries()) {
|
|
724
|
-
try {
|
|
725
|
-
|
|
726
|
-
const decoded = dagCBOR.decode<Uint8Array>(data);
|
|
727
|
-
await this.indexUpdate(path, index, txn, key, decoded, false);
|
|
728
|
-
} catch (err) {
|
|
729
|
-
throw err;
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
/**
|
|
735
|
-
* 从索引中移除条目
|
|
736
|
-
* 确保传入的是旧记录的数据,而非新记录
|
|
737
|
-
*/
|
|
738
|
-
async indexDelete(txn: any, key: Key, originalData: Uint8Array): Promise<void> {
|
|
739
|
-
for (const [path, index] of this.indexes.entries()) {
|
|
740
|
-
try {
|
|
741
|
-
await this.indexUpdate(path, index, txn, key, originalData, true);
|
|
742
|
-
} catch (err) {
|
|
743
|
-
throw err;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
/**
|
|
749
|
-
* 在项目上添加或移除特定索引
|
|
750
|
-
*/
|
|
751
|
-
async indexUpdate(field: string, index: Index, txn: any, key: Key, input: Uint8Array, deleteOp: boolean): Promise<void> {
|
|
752
|
-
try {
|
|
753
|
-
const valueKey = getIndexValue(field, input);
|
|
754
|
-
|
|
755
|
-
const indexKey = indexPrefix.child(this.baseKey()).child(new Key(field)).child(new Key(valueKey.toString().substring(1)));
|
|
756
|
-
let data: Uint8Array | null = null;
|
|
757
|
-
|
|
758
|
-
try {
|
|
759
|
-
data = await txn.get(indexKey);
|
|
760
|
-
} catch (err: any) {
|
|
761
|
-
if (err.code !== 'ERR_NOT_FOUND') {
|
|
762
|
-
throw err;
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
const indexValue = new KeyList();
|
|
767
|
-
if (data) {
|
|
768
|
-
const decoded = JSON.parse(new TextDecoder().decode(data));
|
|
769
|
-
indexValue.fromArray(decoded);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
if (deleteOp) {
|
|
773
|
-
indexValue.remove(key);
|
|
774
|
-
} else {
|
|
775
|
-
if (index.unique) {
|
|
776
|
-
indexValue.remove(key);
|
|
777
|
-
}
|
|
778
|
-
indexValue.add(key);
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if (indexValue.size() === 0) {
|
|
782
|
-
await txn.delete(indexKey);
|
|
783
|
-
} else {
|
|
784
|
-
const encodedValue = new TextEncoder().encode(JSON.stringify(indexValue.toArray()));
|
|
785
|
-
await txn.put(indexKey, encodedValue);
|
|
786
|
-
}
|
|
787
|
-
} catch (err) {
|
|
788
|
-
if (err === ErrNotIndexable) {
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
throw err;
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
/**
|
|
797
|
-
* 返回对输入的字段搜索结果
|
|
798
|
-
*/
|
|
799
|
-
function getIndexValue(field: string, input: Uint8Array): Key {
|
|
800
|
-
const jsonObj = JSON.parse(new TextDecoder().decode(input));
|
|
801
|
-
const value = traverseFieldPathMap(jsonObj, field);
|
|
802
|
-
|
|
803
|
-
if (value === undefined) {
|
|
804
|
-
throw ErrNotIndexable;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
return new Key(String(value));
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
/**
|
|
811
|
-
* KeyList是索引指向的唯一、排序的键切片
|
|
812
|
-
*/
|
|
813
|
-
class KeyList {
|
|
814
|
-
private keys: Uint8Array[] = [];
|
|
815
|
-
|
|
816
|
-
/**
|
|
817
|
-
* 添加键到列表(如果不存在)
|
|
818
|
-
*/
|
|
819
|
-
add(key: Key): void {
|
|
820
|
-
const bytes = key.uint8Array();
|
|
821
|
-
|
|
822
|
-
let i = this.binarySearch(bytes);
|
|
823
|
-
|
|
824
|
-
if (i < this.keys.length && this.bytesEqual(this.keys[i]!, bytes)) {
|
|
825
|
-
return; // 已添加
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
this.keys.splice(i, 0, bytes);
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
/**
|
|
832
|
-
* 从列表中移除键
|
|
833
|
-
*/
|
|
834
|
-
remove(key: Key): void {
|
|
835
|
-
const bytes = key.uint8Array();
|
|
836
|
-
|
|
837
|
-
let i = this.binarySearch(bytes);
|
|
838
|
-
|
|
839
|
-
if (i < this.keys.length && this.keys[i] && this.bytesEqual(this.keys[i], bytes)) {
|
|
840
|
-
this.keys.splice(i, 1);
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
/**
|
|
845
|
-
* 检查键是否在列表中
|
|
846
|
-
*/
|
|
847
|
-
in(key: Key): boolean {
|
|
848
|
-
const bytes = key.uint8Array();
|
|
849
|
-
const i = this.binarySearch(bytes);
|
|
850
|
-
return i < this.keys.length && this.keys[i] !== undefined && this.bytesEqual(this.keys[i], bytes);
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
/**
|
|
854
|
-
* 获取列表大小
|
|
855
|
-
*/
|
|
856
|
-
size(): number {
|
|
857
|
-
return this.keys.length;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
/**
|
|
861
|
-
* 转换为数组
|
|
862
|
-
*/
|
|
863
|
-
toArray(): Uint8Array[] {
|
|
864
|
-
return [...this.keys];
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
/**
|
|
868
|
-
* 从数组填充
|
|
869
|
-
*/
|
|
870
|
-
fromArray(arr: Uint8Array[]): void {
|
|
871
|
-
this.keys = [...arr];
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
/**
|
|
875
|
-
* 二分查找键位置
|
|
876
|
-
*/
|
|
877
|
-
private binarySearch(bytes: Uint8Array): number {
|
|
878
|
-
let low = 0;
|
|
879
|
-
let high = this.keys.length - 1;
|
|
880
|
-
|
|
881
|
-
while (low <= high) {
|
|
882
|
-
const mid = Math.floor((low + high) / 2);
|
|
883
|
-
// Add non-null assertion operator (!) to tell TypeScript this is always defined
|
|
884
|
-
const cmp = this.bytesCompare(this.keys[mid]!, bytes);
|
|
885
|
-
|
|
886
|
-
if (cmp < 0) {
|
|
887
|
-
low = mid + 1;
|
|
888
|
-
} else if (cmp > 0) {
|
|
889
|
-
high = mid - 1;
|
|
890
|
-
} else {
|
|
891
|
-
return mid; // 找到
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
return low; // 未找到,返回插入位置
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
/**
|
|
899
|
-
* 比较两个字节数组
|
|
900
|
-
*/
|
|
901
|
-
private bytesCompare(a: Uint8Array, b: Uint8Array): number {
|
|
902
|
-
const len = Math.min(a.length, b.length);
|
|
903
|
-
|
|
904
|
-
for (let i = 0; i < len; i++) {
|
|
905
|
-
if (a[i] !== b[i]) {
|
|
906
|
-
return a[i]! < b[i]! ? -1 : 1;
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
if (a.length < b.length) return -1;
|
|
911
|
-
if (a.length > b.length) return 1;
|
|
912
|
-
return 0;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
/**
|
|
916
|
-
* 检查两个字节数组是否相等
|
|
917
|
-
*/
|
|
918
|
-
private bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
919
|
-
if (a.length !== b.length) return false;
|
|
920
|
-
|
|
921
|
-
for (let i = 0; i < a.length; i++) {
|
|
922
|
-
if (a[i] !== b[i]) return false;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
return true;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
/**
|
|
933
|
-
* Transaction class for collections
|
|
934
|
-
*/
|
|
935
|
-
export class Txn implements ITxn{
|
|
936
|
-
private actions: Action[] = [];
|
|
937
|
-
private discarded = false;
|
|
938
|
-
private committed = false;
|
|
939
|
-
|
|
940
|
-
constructor(
|
|
941
|
-
private collection: Collection,
|
|
942
|
-
private token?: ThreadToken,
|
|
943
|
-
private readonly readonly: boolean = false
|
|
944
|
-
) {}
|
|
945
|
-
|
|
946
|
-
/**
|
|
947
|
-
* Create new instances
|
|
948
|
-
*/
|
|
949
|
-
async create(...newInstances: Uint8Array[]): Promise<InstanceID[]> {
|
|
950
|
-
const results: InstanceID[] = new Array(newInstances.length);
|
|
951
|
-
|
|
952
|
-
for (let i = 0; i < newInstances.length; i++) {
|
|
953
|
-
if (this.readonly) {
|
|
954
|
-
throw ErrReadonlyTx;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// Copy the data to avoid modifying the input
|
|
958
|
-
const updated = newInstances[i]!.slice(0);
|
|
959
|
-
|
|
960
|
-
// Try to get instance ID, set one if missing
|
|
961
|
-
let id = await getInstanceID(updated);
|
|
962
|
-
if (id === EmptyInstanceID) {
|
|
963
|
-
const result = setNewInstanceID(updated);
|
|
964
|
-
id = result.id;
|
|
965
|
-
const updatedWithId = result.data;
|
|
966
|
-
|
|
967
|
-
// Validate schema
|
|
968
|
-
this.collection.validInstance(updatedWithId);
|
|
969
|
-
|
|
970
|
-
results[i] = id;
|
|
971
|
-
const key = this.collection.baseKey().child(new Key(id));
|
|
972
|
-
|
|
973
|
-
try {
|
|
974
|
-
const exists = await this.collection.db.datastore.has(key);
|
|
975
|
-
if (exists) {
|
|
976
|
-
throw errCantCreateExistingInstance;
|
|
977
|
-
}
|
|
978
|
-
} catch (err) {
|
|
979
|
-
if (err !== errCantCreateExistingInstance) {
|
|
980
|
-
throw new Error(`Error checking if instance exists: ${err instanceof Error ? err.message : String(err)}`);
|
|
981
|
-
}
|
|
982
|
-
throw err;
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
// Set modified timestamp
|
|
986
|
-
const { data: updatedWithTimestamp } = setModifiedTag(updatedWithId);
|
|
987
|
-
|
|
988
|
-
// Add action
|
|
989
|
-
this.actions.push({
|
|
990
|
-
type: CoreActionType.Create,
|
|
991
|
-
instanceID: id,
|
|
992
|
-
collectionName: this.collection.name,
|
|
993
|
-
current: updatedWithTimestamp
|
|
994
|
-
});
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
return results;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
/**
|
|
1002
|
-
* Verify instance changes without saving them
|
|
1003
|
-
*/
|
|
1004
|
-
async verify(...updated: Uint8Array[]): Promise<void> {
|
|
1005
|
-
try {
|
|
1006
|
-
const identity = await this.token?.pubKey();
|
|
1007
|
-
if (!identity) {
|
|
1008
|
-
throw new Error('Identity not found');
|
|
1009
|
-
}
|
|
1010
|
-
const actions = await this.createSaveActions(identity, ...updated);
|
|
1011
|
-
|
|
1012
|
-
const { events } = await this.createEvents(actions);
|
|
1013
|
-
if (events.length === 0) {
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
for (const e of events) {
|
|
1018
|
-
await this.collection.validWrite(identity, e);
|
|
1019
|
-
}
|
|
1020
|
-
} catch (err) {
|
|
1021
|
-
throw new Error(`Verification failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
/**
|
|
1026
|
-
* Save instance changes
|
|
1027
|
-
*/
|
|
1028
|
-
async save(...updated: Uint8Array[]): Promise<void> {
|
|
1029
|
-
try {
|
|
1030
|
-
const identity = await this.token?.pubKey() || null;
|
|
1031
|
-
|
|
1032
|
-
const actions = await this.createSaveActions(identity, ...updated);
|
|
1033
|
-
|
|
1034
|
-
this.actions.push(...actions);
|
|
1035
|
-
} catch (err) {
|
|
1036
|
-
throw new Error(`Save failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
/**
|
|
1041
|
-
* Helper to create save actions
|
|
1042
|
-
*/
|
|
1043
|
-
private async createSaveActions(identity: PubKey|null, ...updated: Uint8Array[]): Promise<Action[]> {
|
|
1044
|
-
const actions: Action[] = [];
|
|
1045
|
-
|
|
1046
|
-
for (let i = 0; i < updated.length; i++) {
|
|
1047
|
-
if (this.readonly) {
|
|
1048
|
-
throw ErrReadonlyTx;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// Copy the data to avoid modifying the input
|
|
1052
|
-
const next = updated[i]!.slice(0);
|
|
1053
|
-
|
|
1054
|
-
// Validate schema
|
|
1055
|
-
this.collection.validInstance(next);
|
|
1056
|
-
|
|
1057
|
-
// Set modified timestamp
|
|
1058
|
-
const { data: nextWithTimestamp } = setModifiedTag(next);
|
|
1059
|
-
|
|
1060
|
-
// Get instance ID
|
|
1061
|
-
const id = await getInstanceID(nextWithTimestamp);
|
|
1062
|
-
if (id === EmptyInstanceID) {
|
|
1063
|
-
throw errMissingInstanceID;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// Get previous version
|
|
1067
|
-
const key = this.collection.baseKey().child(new Key(id));
|
|
1068
|
-
let previous: Uint8Array;
|
|
1069
|
-
|
|
1070
|
-
try {
|
|
1071
|
-
previous = await this.collection.db.datastore.get(key);
|
|
1072
|
-
} catch (err: any) {
|
|
1073
|
-
// If not found, use empty object
|
|
1074
|
-
if (err.code === 'ERR_NOT_FOUND') {
|
|
1075
|
-
previous = new TextEncoder().encode('{}');
|
|
1076
|
-
} else {
|
|
1077
|
-
throw err;
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
// Apply read filter if needed
|
|
1082
|
-
if (previous.length > 0 && identity !== null) {
|
|
1083
|
-
const filtered = await this.collection.filterRead(identity, previous);
|
|
1084
|
-
previous = filtered || previous;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
// Add action
|
|
1088
|
-
actions.push({
|
|
1089
|
-
type: CoreActionType.Save,
|
|
1090
|
-
instanceID: id,
|
|
1091
|
-
collectionName: this.collection.name,
|
|
1092
|
-
previous,
|
|
1093
|
-
current: nextWithTimestamp
|
|
1094
|
-
});
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
return actions;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
/**
|
|
1101
|
-
* Delete instances
|
|
1102
|
-
*/
|
|
1103
|
-
async delete(...ids: InstanceID[]): Promise<void> {
|
|
1104
|
-
for (let i = 0; i < ids.length; i++) {
|
|
1105
|
-
if (this.readonly) {
|
|
1106
|
-
throw ErrReadonlyTx;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
const key = this.collection.baseKey().child(new Key(ids[i]!));
|
|
1110
|
-
const exists = await this.collection.db.datastore.has(key);
|
|
1111
|
-
|
|
1112
|
-
if (!exists) {
|
|
1113
|
-
// Nothing to do
|
|
1114
|
-
continue;
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
// Add action
|
|
1118
|
-
this.actions.push({
|
|
1119
|
-
type: CoreActionType.Delete,
|
|
1120
|
-
instanceID: ids[i]!,
|
|
1121
|
-
collectionName: this.collection.name
|
|
1122
|
-
});
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
/**
|
|
1127
|
-
* Check if instances exist
|
|
1128
|
-
*/
|
|
1129
|
-
async has(...ids: InstanceID[]): Promise<boolean> {
|
|
1130
|
-
const validationResult = await this.collection.db.connector?.validate(this.token);
|
|
1131
|
-
if (validationResult) {
|
|
1132
|
-
throw validationResult;
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
const pk = await this.token?.pubKey();
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
for (let i = 0; i < ids.length; i++) {
|
|
1139
|
-
const key = this.collection.baseKey().child(new Key(ids[i]!));
|
|
1140
|
-
const exists = await this.collection.db.datastore.has(key);
|
|
1141
|
-
|
|
1142
|
-
if (exists) {
|
|
1143
|
-
// If no read filter, instance is accessible
|
|
1144
|
-
if (!this.collection.readFilter) {
|
|
1145
|
-
continue;
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
// Otherwise, check read filter
|
|
1149
|
-
const bytes = await this.collection.db.datastore.get(key);
|
|
1150
|
-
const filtered = await this.collection.filterRead(pk, bytes);
|
|
1151
|
-
|
|
1152
|
-
// If filtered to null, user doesn't have access
|
|
1153
|
-
if (!filtered) {
|
|
1154
|
-
return false;
|
|
1155
|
-
}
|
|
1156
|
-
} else {
|
|
1157
|
-
// Instance doesn't exist
|
|
1158
|
-
return false;
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// All instances exist and are accessible
|
|
1163
|
-
return true;
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
/**
|
|
1167
|
-
* Find instance by ID
|
|
1168
|
-
*/
|
|
1169
|
-
async findByID(id: InstanceID): Promise<Object> {
|
|
1170
|
-
const validationResult = await this.collection.db.connector?.validate(this.token);
|
|
1171
|
-
if (validationResult) {
|
|
1172
|
-
throw validationResult;
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
const key = this.collection.baseKey().child(new Key(id));
|
|
1176
|
-
let bytes: Uint8Array;
|
|
1177
|
-
|
|
1178
|
-
try {
|
|
1179
|
-
bytes = await this.collection.db.datastore.get(key);
|
|
1180
|
-
} catch (err: any) {
|
|
1181
|
-
if (err.code === 'ERR_NOT_FOUND') {
|
|
1182
|
-
throw ErrInstanceNotFound;
|
|
1183
|
-
}
|
|
1184
|
-
throw err;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
const pk = await this.token?.pubKey();
|
|
1188
|
-
|
|
1189
|
-
const filtered = await this.collection.filterRead(pk, bytes);
|
|
1190
|
-
|
|
1191
|
-
if (!filtered) {
|
|
1192
|
-
throw ErrInstanceNotFound;
|
|
1193
|
-
}
|
|
1194
|
-
let parsedValue;
|
|
1195
|
-
try {
|
|
1196
|
-
parsedValue = JSON.parse(new TextDecoder().decode(filtered));
|
|
1197
|
-
} catch (err) {
|
|
1198
|
-
console.warn("Failed to parse JSON for sorting:", err);
|
|
1199
|
-
parsedValue = {};
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
return parsedValue;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
/**
|
|
1206
|
-
* Find instances matching a query
|
|
1207
|
-
*/
|
|
1208
|
-
async find(q?: Query): Promise<Object[]> {
|
|
1209
|
-
try {
|
|
1210
|
-
// 验证令牌
|
|
1211
|
-
const validationError = await this.collection.db.connector?.validate(this.token);
|
|
1212
|
-
if (validationError) {
|
|
1213
|
-
throw validationError;
|
|
1214
|
-
}
|
|
1215
|
-
if (!q) {
|
|
1216
|
-
q = new Query();
|
|
1217
|
-
}
|
|
1218
|
-
const validationResult = q.validate();
|
|
1219
|
-
if (validationResult) {
|
|
1220
|
-
throw new Error(`Invalid query: ${validationResult.message}`);
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
const txn = await this.collection.db.datastore.newTransactionExtended(true);
|
|
1224
|
-
|
|
1225
|
-
try {
|
|
1226
|
-
// 创建迭代器
|
|
1227
|
-
const iter = await this.newIterator(txn, this.collection.baseKey(), q);
|
|
1228
|
-
|
|
1229
|
-
try {
|
|
1230
|
-
// 获取公钥
|
|
1231
|
-
const pk = await this.token?.pubKey();
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
// 存储结果
|
|
1235
|
-
const values: MarshaledResult[] = [];
|
|
1236
|
-
|
|
1237
|
-
// 跟踪返回的实际值数量(考虑到读取过滤器和查询中的任何索引)
|
|
1238
|
-
let count = 0;
|
|
1239
|
-
|
|
1240
|
-
// 迭代所有结果
|
|
1241
|
-
while (true) {
|
|
1242
|
-
const result = await iter.next();
|
|
1243
|
-
if (!result || !result.done === false) {
|
|
1244
|
-
break;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
// 应用读取过滤器
|
|
1248
|
-
let filteredValue: Uint8Array | null = null;
|
|
1249
|
-
try {
|
|
1250
|
-
filteredValue = await this.collection.filterRead(pk, result.value);
|
|
1251
|
-
} catch (err) {
|
|
1252
|
-
throw new Error(`Filter read error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
if (filteredValue) {
|
|
1256
|
-
// 只计算未被读取过滤器过滤掉的有效值
|
|
1257
|
-
count++;
|
|
1258
|
-
|
|
1259
|
-
if (count > q.skip) {
|
|
1260
|
-
// 解析JSON以便之后排序
|
|
1261
|
-
let parsedValue;
|
|
1262
|
-
try {
|
|
1263
|
-
parsedValue = JSON.parse(new TextDecoder().decode(filteredValue));
|
|
1264
|
-
} catch (err) {
|
|
1265
|
-
console.warn("Failed to parse JSON for sorting:", err);
|
|
1266
|
-
parsedValue = {};
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
values.push({
|
|
1270
|
-
instanceID: result.instanceID,
|
|
1271
|
-
key: result.key,
|
|
1272
|
-
value: filteredValue,
|
|
1273
|
-
marshaledValue: parsedValue
|
|
1274
|
-
});
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
// 如果达到限制,则停止
|
|
1279
|
-
if (q.limit > 0 && values.length === q.limit) {
|
|
1280
|
-
break;
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
// 如果需要排序且不是按ID排序
|
|
1285
|
-
if (q.sort.fieldPath && q.sort.fieldPath !== idFieldName) {
|
|
1286
|
-
// let wrongField = false;
|
|
1287
|
-
// let cantCompare = false;
|
|
1288
|
-
|
|
1289
|
-
// 对结果进行排序
|
|
1290
|
-
values.sort((a, b) => {
|
|
1291
|
-
// 获取排序字段的值
|
|
1292
|
-
const fieldA = traverseFieldPathMap(a.marshaledValue || {}, q!.sort.fieldPath);
|
|
1293
|
-
const fieldB = traverseFieldPathMap(b.marshaledValue || {}, q!.sort.fieldPath);
|
|
1294
|
-
|
|
1295
|
-
// 处理缺少值的情况
|
|
1296
|
-
if (fieldA === undefined || fieldB === undefined) {
|
|
1297
|
-
// wrongField = true;
|
|
1298
|
-
|
|
1299
|
-
// 使null值排在最前或最后
|
|
1300
|
-
if (fieldA === undefined && fieldB === undefined) return 0;
|
|
1301
|
-
return fieldA === undefined ? -1 : 1;
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
try {
|
|
1305
|
-
// 比较两个字段值
|
|
1306
|
-
let result = compare(fieldA, fieldB);
|
|
1307
|
-
|
|
1308
|
-
// 如果是降序,则反转比较结果
|
|
1309
|
-
if (q!.sort.desc) {
|
|
1310
|
-
result *= -1;
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
return result;
|
|
1314
|
-
} catch (err) {
|
|
1315
|
-
// cantCompare = true;
|
|
1316
|
-
return 0;
|
|
1317
|
-
}
|
|
1318
|
-
});
|
|
1319
|
-
|
|
1320
|
-
// // 处理排序错误
|
|
1321
|
-
// if (wrongField) {
|
|
1322
|
-
// throw ErrInvalidSortingField;
|
|
1323
|
-
// }
|
|
1324
|
-
|
|
1325
|
-
// if (cantCompare) {
|
|
1326
|
-
// throw new Error("Can't compare while sorting");
|
|
1327
|
-
// }
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
// 提取最终结果
|
|
1331
|
-
const results = values.map(v => v.marshaledValue);
|
|
1332
|
-
return results as Object[];
|
|
1333
|
-
|
|
1334
|
-
} finally {
|
|
1335
|
-
// 清理迭代器资源
|
|
1336
|
-
if (iter && typeof iter.close === 'function') {
|
|
1337
|
-
await iter.close();
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
} finally {
|
|
1341
|
-
// 确保丢弃事务
|
|
1342
|
-
txn.discard();
|
|
1343
|
-
}
|
|
1344
|
-
} catch (err) {
|
|
1345
|
-
throw new Error(`Find operation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
/**
|
|
1350
|
-
* 创建一个适合查询的迭代器
|
|
1351
|
-
*/
|
|
1352
|
-
private async newIterator(txn: any, baseKey: Key, q: Query): Promise<any> {
|
|
1353
|
-
// 这里实现迭代器创建逻辑
|
|
1354
|
-
// 注意: 这是一个占位符,具体实现取决于你的数据存储实现
|
|
1355
|
-
|
|
1356
|
-
// 示例实现:
|
|
1357
|
-
const queryOptions: any = {
|
|
1358
|
-
prefix: baseKey.toString()
|
|
1359
|
-
};
|
|
1360
|
-
|
|
1361
|
-
// 如果有ID索引,使用索引查询
|
|
1362
|
-
if (q.index) {
|
|
1363
|
-
queryOptions.index = q.index;
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
// 如果需要查找特定ID
|
|
1367
|
-
if (q.seek) {
|
|
1368
|
-
queryOptions.start = baseKey.child(new Key(q.seek)).toString();
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
// 创建底层查询
|
|
1372
|
-
const results = await txn.query(queryOptions);
|
|
1373
|
-
|
|
1374
|
-
// 返回一个支持 next() 和 close() 的迭代器
|
|
1375
|
-
return {
|
|
1376
|
-
async next() {
|
|
1377
|
-
const result = await results.next();
|
|
1378
|
-
if (result.done) {
|
|
1379
|
-
return { done: true };
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
// 检查是否匹配查询条件
|
|
1383
|
-
if (result.value && q && result.value.value) {
|
|
1384
|
-
try {
|
|
1385
|
-
const decodedValue = JSON.parse(new TextDecoder().decode(result.value.value));
|
|
1386
|
-
if (!q.match(decodedValue)) {
|
|
1387
|
-
// 不匹配,跳过这个结果
|
|
1388
|
-
return this.next();
|
|
1389
|
-
}
|
|
1390
|
-
} catch (err) {
|
|
1391
|
-
console.warn("Error parsing JSON in query:", err);
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
return {
|
|
1396
|
-
done: false,
|
|
1397
|
-
key: result.value.key,
|
|
1398
|
-
instanceID: new Key(result.value.key).name(),
|
|
1399
|
-
value: result.value.value
|
|
1400
|
-
};
|
|
1401
|
-
},
|
|
1402
|
-
|
|
1403
|
-
async close() {
|
|
1404
|
-
// 关闭底层资源
|
|
1405
|
-
if (results && typeof results.return === 'function') {
|
|
1406
|
-
await results.return();
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
};
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
/**
|
|
1413
|
-
* Get instances modified since a specific time
|
|
1414
|
-
*
|
|
1415
|
-
* The _mod field tracks modified instances, but not those that have been deleted, so we need
|
|
1416
|
-
* to query the dispatcher for all (unique) instances in this collection that have been modified
|
|
1417
|
-
* at all since `time`.
|
|
1418
|
-
*/
|
|
1419
|
-
async modifiedSince(time: number): Promise<InstanceID[]> {
|
|
1420
|
-
// Create a read-only transaction
|
|
1421
|
-
const txn = await this.collection.db.datastore.newTransactionExtended(true);
|
|
1422
|
-
try {
|
|
1423
|
-
const collectionFilter: QueryFilter = (item: any) => {
|
|
1424
|
-
const keyString = item.key.toString();
|
|
1425
|
-
const key = new Key(keyString);
|
|
1426
|
-
return key.type() === this.collection.name;
|
|
1427
|
-
};
|
|
1428
|
-
|
|
1429
|
-
// Convert time to string for key operations
|
|
1430
|
-
const timestr = time.toString();
|
|
1431
|
-
// Create the query
|
|
1432
|
-
const res = txn.queryExtended({
|
|
1433
|
-
prefix: dsDispatcherPrefix.toString(),
|
|
1434
|
-
filters: [collectionFilter],
|
|
1435
|
-
seekPrefix: dsDispatcherPrefix.child(new Key(timestr)).toString()
|
|
1436
|
-
});
|
|
1437
|
-
|
|
1438
|
-
// Create a set to store unique instance IDs
|
|
1439
|
-
const instanceIdSet = new Set<InstanceID>();
|
|
1440
|
-
|
|
1441
|
-
// Process query results
|
|
1442
|
-
for await (const entry of res) {
|
|
1443
|
-
const id = new Key(entry.key).name();
|
|
1444
|
-
instanceIdSet.add(id as InstanceID);
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
// Convert set to array
|
|
1448
|
-
return Array.from(instanceIdSet);
|
|
1449
|
-
} finally {
|
|
1450
|
-
// Make sure to discard the transaction
|
|
1451
|
-
txn.discard();
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
/**
|
|
1455
|
-
* Commit the transaction
|
|
1456
|
-
*/
|
|
1457
|
-
async commit(): Promise<void> {
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
try {
|
|
1461
|
-
const { events, node } = await this.createEvents(this.actions);
|
|
1462
|
-
if (!node) {
|
|
1463
|
-
return;
|
|
1464
|
-
}
|
|
1465
|
-
await this.collection.db.connector?.createNetRecord(node, this.token);
|
|
1466
|
-
await this.collection.db.dispatcher?.dispatch(events);
|
|
1467
|
-
this.committed = true;
|
|
1468
|
-
} catch (err) {
|
|
1469
|
-
throw new Error(`Commit failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
/**
|
|
1474
|
-
* Discard the transaction
|
|
1475
|
-
*/
|
|
1476
|
-
discard(): void {
|
|
1477
|
-
this.discarded = true;
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
/**
|
|
1481
|
-
* Refresh collection reference
|
|
1482
|
-
*/
|
|
1483
|
-
refreshCollection(): void {
|
|
1484
|
-
const c = this.collection.db.collections.get(this.collection.name);
|
|
1485
|
-
if (!c) {
|
|
1486
|
-
throw ErrCollectionNotFound;
|
|
1487
|
-
}
|
|
1488
|
-
this.collection = c as Collection;
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
/**
|
|
1492
|
-
* Create events from actions
|
|
1493
|
-
*/
|
|
1494
|
-
private async createEvents(actions: Action[]): Promise<{ events: Event[], node: IPLDNode | null }> {
|
|
1495
|
-
if (this.discarded || this.committed) {
|
|
1496
|
-
throw errAlreadyDiscardedCommitedTxn;
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
try {
|
|
1500
|
-
const [events, node] = await this.collection.db.eventcodec.create(actions);
|
|
1501
|
-
|
|
1502
|
-
if (events.length === 0 && node) {
|
|
1503
|
-
return { events: [], node: null };
|
|
1504
|
-
}
|
|
1505
|
-
// const node = await cbornode.wrapObject(nodeData);
|
|
1506
|
-
// if (!node) {
|
|
1507
|
-
// throw new Error('Failed to wrap node data');
|
|
1508
|
-
// }
|
|
1509
|
-
|
|
1510
|
-
return { events, node };
|
|
1511
|
-
} catch (err) {
|
|
1512
|
-
throw new Error(`Error creating events: ${err instanceof Error ? err.message : String(err)}`);
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
/**
|
|
1518
|
-
* Create a new collection
|
|
1519
|
-
*/
|
|
1520
|
-
export async function newCollection(db: IDB, config: ICollectionConfig): Promise<Collection> {
|
|
1521
|
-
// Validate name
|
|
1522
|
-
if (config.name && !nameRx.test(config.name)) {
|
|
1523
|
-
throw ErrInvalidName;
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
// Validate schema
|
|
1527
|
-
const idType = getSchemaTypeAtPath(config.schema, idFieldName);
|
|
1528
|
-
if (!idType || idType.type !== 'string') {
|
|
1529
|
-
throw ErrInvalidCollectionSchema;
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
// Create collection
|
|
1533
|
-
return new Collection(
|
|
1534
|
-
config.name,
|
|
1535
|
-
config.schema,
|
|
1536
|
-
db,
|
|
1537
|
-
config.writeValidator,
|
|
1538
|
-
config.readFilter
|
|
1539
|
-
);
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
// Helper functions
|
|
1543
|
-
|
|
1544
|
-
/**
|
|
1545
|
-
* Get schema type at a path
|
|
1546
|
-
*/
|
|
1547
|
-
function getSchemaTypeAtPath(schema: any, path: string): { type: string } | null {
|
|
1548
|
-
const parts = path.split('.');
|
|
1549
|
-
let current = schema;
|
|
1550
|
-
|
|
1551
|
-
for (const part of parts) {
|
|
1552
|
-
const props = getSchemaTypeProperties(current, schema.definitions);
|
|
1553
|
-
current = props[part];
|
|
1554
|
-
|
|
1555
|
-
if (!current) {
|
|
1556
|
-
throw ErrInvalidCollectionSchemaPath;
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
return current;
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
/**
|
|
1564
|
-
* Get schema type properties
|
|
1565
|
-
*/
|
|
1566
|
-
function getSchemaTypeProperties(schema: any, definitions: any): Record<string, any> {
|
|
1567
|
-
if (!schema) {
|
|
1568
|
-
return {};
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
let properties = schema.properties || {};
|
|
1572
|
-
|
|
1573
|
-
if (schema.$ref) {
|
|
1574
|
-
const parts = schema.$ref.split('/');
|
|
1575
|
-
if (parts.length > 0) {
|
|
1576
|
-
const defName = parts[parts.length - 1];
|
|
1577
|
-
const def = definitions?.[defName];
|
|
1578
|
-
|
|
1579
|
-
if (def) {
|
|
1580
|
-
properties = def.properties || {};
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
return properties;
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
/**
|
|
1589
|
-
* Extract instance ID from data
|
|
1590
|
-
*/
|
|
1591
|
-
async function getInstanceID(data: Uint8Array): Promise<InstanceID> {
|
|
1592
|
-
try {
|
|
1593
|
-
const instance = JSON.parse(new TextDecoder().decode(data));
|
|
1594
|
-
|
|
1595
|
-
if (!instance._id) {
|
|
1596
|
-
return EmptyInstanceID;
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
return instance._id;
|
|
1600
|
-
} catch (err) {
|
|
1601
|
-
throw new Error(`Error getting instance ID: ${err instanceof Error ? err.message : String(err)}`);
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
/**
|
|
1606
|
-
* Set a new instance ID
|
|
1607
|
-
*/
|
|
1608
|
-
function setNewInstanceID(data: Uint8Array): { id: InstanceID, data: Uint8Array } {
|
|
1609
|
-
const id = NewInstanceID();
|
|
1610
|
-
|
|
1611
|
-
try {
|
|
1612
|
-
const instance = JSON.parse(new TextDecoder().decode(data));
|
|
1613
|
-
instance._id = id;
|
|
1614
|
-
|
|
1615
|
-
return {
|
|
1616
|
-
id,
|
|
1617
|
-
data: new TextEncoder().encode(JSON.stringify(instance))
|
|
1618
|
-
};
|
|
1619
|
-
} catch (err) {
|
|
1620
|
-
throw new Error(`Error setting instance ID: ${err instanceof Error ? err.message : String(err)}`);
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
/**
|
|
1625
|
-
* 为对象设置修改时间戳标签 (纳秒精度)
|
|
1626
|
-
* @param data 包含原始数据的字节数组
|
|
1627
|
-
* @returns 包含新时间戳和更新后数据的对象
|
|
1628
|
-
*/
|
|
1629
|
-
function setModifiedTag(data: Uint8Array): { time: bigint, data: Uint8Array } {
|
|
1630
|
-
// 生成纳秒级时间戳 (等同于 Go 的 time.Now().UnixNano())
|
|
1631
|
-
const timeMs = BigInt(Date.now());
|
|
1632
|
-
const randomNs = BigInt(Math.floor(Math.random() * 1000000));
|
|
1633
|
-
// 转换为纳秒
|
|
1634
|
-
const time = timeMs * 1000000n + randomNs;
|
|
1635
|
-
|
|
1636
|
-
try {
|
|
1637
|
-
// 解析输入数据为 JSON 对象
|
|
1638
|
-
const instance = JSON.parse(new TextDecoder().decode(data));
|
|
1639
|
-
|
|
1640
|
-
// 字段名与 Go 代码中相同
|
|
1641
|
-
const modFieldName = "_mod";
|
|
1642
|
-
|
|
1643
|
-
// 添加或更新 _mod 字段
|
|
1644
|
-
// 注意:存储时将 BigInt 转换为数字,这可能会在某些极端情况下丢失精度
|
|
1645
|
-
// 但对于一般应用已经足够了
|
|
1646
|
-
instance[modFieldName] = time;
|
|
1647
|
-
|
|
1648
|
-
// 序列化回字节数组
|
|
1649
|
-
const modifiedData = new TextEncoder().encode(jsonStringify(instance));
|
|
1650
|
-
|
|
1651
|
-
return {
|
|
1652
|
-
time,
|
|
1653
|
-
data: modifiedData
|
|
1654
|
-
};
|
|
1655
|
-
} catch (err) {
|
|
1656
|
-
// 处理错误,但避免使用 log.Fatal 这样的致命错误
|
|
1657
|
-
console.error(`修改标签设置错误: ${err instanceof Error ? err.message : String(err)}`);
|
|
1658
|
-
throw new Error(`设置 _mod 字段失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
/**
|
|
1663
|
-
* Parse JSON from bytes
|
|
1664
|
-
*/
|
|
1665
|
-
function parseJSON(data: Uint8Array): any {
|
|
1666
|
-
return JSON.parse(new TextDecoder().decode(data));
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
/**
|
|
1670
|
-
* 迭代器类用于查询结果的迭代
|
|
1671
|
-
*/
|
|
1672
|
-
class Iterator {
|
|
1673
|
-
nextKeys: () => Promise<Key[]>;
|
|
1674
|
-
txn: any;
|
|
1675
|
-
query: Query;
|
|
1676
|
-
keyCache: Key[] = [];
|
|
1677
|
-
iter: any;
|
|
1678
|
-
|
|
1679
|
-
constructor(txn: any, query: Query) {
|
|
1680
|
-
this.txn = txn;
|
|
1681
|
-
this.query = query;
|
|
1682
|
-
this.nextKeys = async () => { return []; };
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
/**
|
|
1686
|
-
* 返回下一个符合迭代器条件的键值对
|
|
1687
|
-
* 如果有错误,ok 为 false,result.error 将返回错误
|
|
1688
|
-
*/
|
|
1689
|
-
async nextSync(): Promise<{ result: any, marshaledValue?: any } | null> {
|
|
1690
|
-
if (this.query.index === "") {
|
|
1691
|
-
const value: any = {
|
|
1692
|
-
result: {
|
|
1693
|
-
entry: {},
|
|
1694
|
-
error: null
|
|
1695
|
-
}
|
|
1696
|
-
};
|
|
1697
|
-
let ok = false;
|
|
1698
|
-
|
|
1699
|
-
for await (const res of this.iter.next()) {
|
|
1700
|
-
try {
|
|
1701
|
-
const val = JSON.parse(new TextDecoder().decode(res.value));
|
|
1702
|
-
ok = this.query.match(val);
|
|
1703
|
-
|
|
1704
|
-
if (value.result.error) {
|
|
1705
|
-
break;
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
if (ok) {
|
|
1709
|
-
return {
|
|
1710
|
-
result: res,
|
|
1711
|
-
marshaledValue: val
|
|
1712
|
-
};
|
|
1713
|
-
}
|
|
1714
|
-
} catch (error) {
|
|
1715
|
-
value.result.error = error;
|
|
1716
|
-
break;
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
return ok ? value : null;
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
if (this.keyCache.length === 0) {
|
|
1724
|
-
try {
|
|
1725
|
-
const newKeys = await this.nextKeys();
|
|
1726
|
-
|
|
1727
|
-
if (newKeys.length === 0) {
|
|
1728
|
-
return {
|
|
1729
|
-
result: {
|
|
1730
|
-
entry: {},
|
|
1731
|
-
error: null
|
|
1732
|
-
}
|
|
1733
|
-
};
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
this.keyCache = [...this.keyCache, ...newKeys];
|
|
1737
|
-
} catch (error) {
|
|
1738
|
-
return {
|
|
1739
|
-
result: {
|
|
1740
|
-
entry: {},
|
|
1741
|
-
error: error
|
|
1742
|
-
}
|
|
1743
|
-
};
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
const key = this.keyCache[0];
|
|
1748
|
-
this.keyCache = this.keyCache.slice(1);
|
|
1749
|
-
|
|
1750
|
-
try {
|
|
1751
|
-
const value = await this.txn.get(key);
|
|
1752
|
-
|
|
1753
|
-
return {
|
|
1754
|
-
result: {
|
|
1755
|
-
entry: {
|
|
1756
|
-
key: key!.toString(),
|
|
1757
|
-
value: value
|
|
1758
|
-
},
|
|
1759
|
-
error: null
|
|
1760
|
-
}
|
|
1761
|
-
};
|
|
1762
|
-
} catch (error) {
|
|
1763
|
-
return {
|
|
1764
|
-
result: {
|
|
1765
|
-
entry: {},
|
|
1766
|
-
error: error
|
|
1767
|
-
}
|
|
1768
|
-
};
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
|
-
|
|
1772
|
-
/**
|
|
1773
|
-
* 关闭迭代器并释放资源
|
|
1774
|
-
*/
|
|
1775
|
-
async close(): Promise<void> {
|
|
1776
|
-
if (this.iter && typeof this.iter.close === 'function') {
|
|
1777
|
-
await this.iter.close();
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
/**
|
|
1783
|
-
* 创建新的迭代器
|
|
1784
|
-
*/
|
|
1785
|
-
export async function newIterator(txn: any, baseKey: Key, q: Query): Promise<Iterator> {
|
|
1786
|
-
const i = new Iterator(txn, q);
|
|
1787
|
-
|
|
1788
|
-
let prefix: Key;
|
|
1789
|
-
if (!q.index) {
|
|
1790
|
-
prefix = baseKey;
|
|
1791
|
-
} else {
|
|
1792
|
-
prefix = indexPrefix.child(baseKey).child(new Key(q.index));
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
const dsq: any = {
|
|
1796
|
-
query: {
|
|
1797
|
-
prefix: prefix.toString(),
|
|
1798
|
-
// 由于 readFilters 的存在,我们事先不知道需要跳过/限制多少,
|
|
1799
|
-
// 所以这里不使用 Skip 和 Limit
|
|
1800
|
-
// limit: q.limit,
|
|
1801
|
-
// offset: q.skip,
|
|
1802
|
-
}
|
|
1803
|
-
};
|
|
1804
|
-
|
|
1805
|
-
if (q.sort.fieldPath === idFieldName) {
|
|
1806
|
-
if (q.sort.desc) {
|
|
1807
|
-
dsq.orders = [{ orderByKeyDescending: true }];
|
|
1808
|
-
} else {
|
|
1809
|
-
dsq.orders = [{ orderByKey: true }];
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
if (q.seek) {
|
|
1814
|
-
dsq.seekPrefix = prefix.child(new Key(q.seek)).toString();
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
try {
|
|
1818
|
-
const iter = await txn.queryExtended(dsq);
|
|
1819
|
-
i.iter = iter;
|
|
1820
|
-
|
|
1821
|
-
// 未指定键字段或索引,通过基本的"迭代器"传递
|
|
1822
|
-
if (!q.index) {
|
|
1823
|
-
i.nextKeys = async () => {
|
|
1824
|
-
return [];
|
|
1825
|
-
};
|
|
1826
|
-
return i;
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
// 索引字段,从索引获取键
|
|
1830
|
-
let first = true;
|
|
1831
|
-
i.nextKeys = async () => {
|
|
1832
|
-
const nKeys: Key[] = [];
|
|
1833
|
-
|
|
1834
|
-
while (nKeys.length < iteratorKeyMinCacheSize) {
|
|
1835
|
-
const result = await i.iter.nextSync();
|
|
1836
|
-
if (!result) {
|
|
1837
|
-
if (first) {
|
|
1838
|
-
throw ErrIndexNotFound;
|
|
1839
|
-
}
|
|
1840
|
-
return nKeys;
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
first = false;
|
|
1844
|
-
// result.key 包含索引值,先在这里提取
|
|
1845
|
-
const key = new Key(result.result.entry.key);
|
|
1846
|
-
const base = prefix.name();
|
|
1847
|
-
const name = key.name();
|
|
1848
|
-
|
|
1849
|
-
let val: any = name;
|
|
1850
|
-
if (isValidJSON(name)) {
|
|
1851
|
-
const parsed = JSON.parse(name);
|
|
1852
|
-
val = parsed !== null ? parsed : name;
|
|
1853
|
-
} else {
|
|
1854
|
-
val = name;
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
try {
|
|
1858
|
-
// 使用 JSON 构建文档
|
|
1859
|
-
const doc = JSON.stringify({ [base]: val });
|
|
1860
|
-
const value = JSON.parse(doc);
|
|
1861
|
-
|
|
1862
|
-
// 匹配查询
|
|
1863
|
-
const ok = q.match(value);
|
|
1864
|
-
|
|
1865
|
-
if (ok) {
|
|
1866
|
-
// 解码索引值
|
|
1867
|
-
const indexValue = decodeKeyList(result.result.entry.value);
|
|
1868
|
-
|
|
1869
|
-
for (const v of indexValue) {
|
|
1870
|
-
nKeys.push(new Key(new TextDecoder().decode(v)));
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
} catch (err) {
|
|
1874
|
-
// 为了让业务逻辑正常进行,这里不报告错误,而是跳过
|
|
1875
|
-
continue;
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
return nKeys;
|
|
1880
|
-
};
|
|
1881
|
-
|
|
1882
|
-
return i;
|
|
1883
|
-
} catch (error) {
|
|
1884
|
-
throw error;
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
/**
|
|
1889
|
-
* 辅助函数:检查字符串是否是有效的 JSON
|
|
1890
|
-
*/
|
|
1891
|
-
function isValidJSON(str: string): boolean {
|
|
1892
|
-
try {
|
|
1893
|
-
JSON.parse(str);
|
|
1894
|
-
return true;
|
|
1895
|
-
} catch {
|
|
1896
|
-
return false;
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
/**
|
|
1901
|
-
* 辅助函数:解码键列表
|
|
1902
|
-
*/
|
|
1903
|
-
function decodeKeyList(data?: Uint8Array): Uint8Array[] {
|
|
1904
|
-
if (!data) return [];
|
|
1905
|
-
try {
|
|
1906
|
-
return JSON.parse(new TextDecoder().decode(data));
|
|
1907
|
-
} catch {
|
|
1908
|
-
return [];
|
|
1909
|
-
}
|
|
1910
|
-
}
|