@tinycloud/replica 0.1.1-beta.0 → 0.2.0-beta.2
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/README.md +2 -0
- package/dist/browser.d.ts +3 -1
- package/dist/browser.js +27 -0
- package/dist/browser.js.map +1 -1
- package/dist/index.cjs +71 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -7
- package/dist/index.d.ts +10 -7
- package/dist/index.js +70 -10
- package/dist/index.js.map +1 -1
- package/dist/replica.worker.d.ts +16 -0
- package/dist/replica.worker.js +80 -14
- package/dist/replica.worker.js.map +1 -1
- package/dist/sqlite.cjs +26 -0
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.js +26 -0
- package/dist/sqlite.js.map +1 -1
- package/package.json +3 -2
package/dist/replica.worker.js
CHANGED
|
@@ -9434,6 +9434,7 @@ var KVService = class extends BaseService {
|
|
|
9434
9434
|
"kv"
|
|
9435
9435
|
));
|
|
9436
9436
|
}
|
|
9437
|
+
const requestPrefix = options.prefix === "/" ? "" : options.prefix;
|
|
9437
9438
|
const headers = {};
|
|
9438
9439
|
if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
|
|
9439
9440
|
if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
|
|
@@ -9443,7 +9444,7 @@ var KVService = class extends BaseService {
|
|
|
9443
9444
|
const request = this.createRequestSignal(options.signal, options.timeout);
|
|
9444
9445
|
try {
|
|
9445
9446
|
const response = await this.invokeOperation(
|
|
9446
|
-
|
|
9447
|
+
requestPrefix,
|
|
9447
9448
|
KVAction.SYNC,
|
|
9448
9449
|
void 0,
|
|
9449
9450
|
request.signal,
|
|
@@ -9459,7 +9460,7 @@ var KVService = class extends BaseService {
|
|
|
9459
9460
|
} catch {
|
|
9460
9461
|
body = void 0;
|
|
9461
9462
|
}
|
|
9462
|
-
const page = this.normalizeChangesResponse(body);
|
|
9463
|
+
const page = this.normalizeChangesResponse(body, options.prefix);
|
|
9463
9464
|
if (!page) {
|
|
9464
9465
|
return err(serviceError(
|
|
9465
9466
|
ErrorCodes.NETWORK_ERROR,
|
|
@@ -9539,7 +9540,7 @@ var KVService = class extends BaseService {
|
|
|
9539
9540
|
{ meta }
|
|
9540
9541
|
));
|
|
9541
9542
|
}
|
|
9542
|
-
normalizeChangesResponse(data) {
|
|
9543
|
+
normalizeChangesResponse(data, requestedPrefix) {
|
|
9543
9544
|
if (!isJsonObject(data)) return void 0;
|
|
9544
9545
|
const { changes, more, cursor, source, authority } = data;
|
|
9545
9546
|
if (!Array.isArray(changes) || typeof more !== "boolean" || typeof cursor !== "string" || !isJsonObject(source) || typeof source.nodeDid !== "string" || typeof source.space !== "string" || typeof source.prefix !== "string" || !isJsonObject(authority)) {
|
|
@@ -9571,7 +9572,11 @@ var KVService = class extends BaseService {
|
|
|
9571
9572
|
changes: parsed,
|
|
9572
9573
|
more,
|
|
9573
9574
|
cursor,
|
|
9574
|
-
source: {
|
|
9575
|
+
source: {
|
|
9576
|
+
nodeDid: source.nodeDid,
|
|
9577
|
+
space: source.space,
|
|
9578
|
+
prefix: requestedPrefix === "/" && source.prefix === "" ? "/" : source.prefix
|
|
9579
|
+
},
|
|
9575
9580
|
authority: { notBefore, expiresAt, retainUntil }
|
|
9576
9581
|
};
|
|
9577
9582
|
}
|
|
@@ -14553,6 +14558,8 @@ var ReplicaErrorCode = {
|
|
|
14553
14558
|
GRANT_NOT_COVERING: "GRANT_NOT_COVERING",
|
|
14554
14559
|
GRANT_NOT_YET_VALID: "GRANT_NOT_YET_VALID",
|
|
14555
14560
|
GRANT_EXPIRED: "GRANT_EXPIRED",
|
|
14561
|
+
/** The configured node does not advertise whole-space KV sync support. */
|
|
14562
|
+
REPLICATION_ROOT_UNSUPPORTED: "REPLICATION_ROOT_UNSUPPORTED",
|
|
14556
14563
|
GRANT_REVOKED: "GRANT_REVOKED",
|
|
14557
14564
|
/** The node refused the invocation for a reason other than revocation. */
|
|
14558
14565
|
GRANT_UNAUTHORIZED: "GRANT_UNAUTHORIZED",
|
|
@@ -14593,18 +14600,39 @@ function isReplicaError(error, code) {
|
|
|
14593
14600
|
return typeof error === "object" && error !== null && error[REPLICA_ERROR] === true && (code === void 0 || error.code === code);
|
|
14594
14601
|
}
|
|
14595
14602
|
|
|
14603
|
+
// ../sdk-services/dist/kv/replication/prefix.js
|
|
14604
|
+
function assertFolderPrefix(prefix) {
|
|
14605
|
+
const validFolder = typeof prefix === "string" && prefix.length > 0 && !prefix.startsWith("/") && !prefix.includes("//") && prefix.endsWith("/");
|
|
14606
|
+
if (prefix !== "/" && !validFolder) {
|
|
14607
|
+
const folderHint = typeof prefix === "string" ? prefix.replace(/^\/+/, "").replace(/\/+$/, "") : "";
|
|
14608
|
+
const suggestion = folderHint.length > 0 && !folderHint.includes("//") ? ` Use ${JSON.stringify(`${folderHint}/`)}.` : ' Use "/" or a slash-terminated folder path such as "notes/".';
|
|
14609
|
+
throw new TypeError(`Replication prefix must be "/" or a non-empty slash-terminated folder path without empty segments.${suggestion}`);
|
|
14610
|
+
}
|
|
14611
|
+
}
|
|
14612
|
+
|
|
14596
14613
|
// src/scope.ts
|
|
14614
|
+
function assertFolderPrefix2(prefix) {
|
|
14615
|
+
try {
|
|
14616
|
+
assertFolderPrefix(prefix);
|
|
14617
|
+
} catch {
|
|
14618
|
+
const folderHint = typeof prefix === "string" ? prefix.replace(/^\/+/, "").replace(/\/+$/, "") : "";
|
|
14619
|
+
const suggestion = prefix === "" ? 'use "/" for the whole space' : folderHint.length > 0 && !folderHint.includes("//") ? `use ${JSON.stringify(`${folderHint}/`)}` : 'use "/" for the whole space or a folder path such as "notes/"';
|
|
14620
|
+
throw new ReplicaError(
|
|
14621
|
+
ReplicaErrorCode.INVALID_ARGUMENT,
|
|
14622
|
+
`Replication prefix ${JSON.stringify(prefix)} must be "/" or a non-empty folder path ending in / without empty segments; ${suggestion}. Run \`tc replica reset --replica <name> --purge\` and recreate it with the folder prefix.`
|
|
14623
|
+
);
|
|
14624
|
+
}
|
|
14625
|
+
}
|
|
14597
14626
|
function kvPrefixCovers(prefix, key) {
|
|
14598
|
-
|
|
14599
|
-
if (prefix.endsWith("/")) return key.startsWith(prefix);
|
|
14600
|
-
return key === prefix || key.startsWith(`${prefix}/`);
|
|
14627
|
+
return prefix === "/" || key.startsWith(prefix);
|
|
14601
14628
|
}
|
|
14602
14629
|
function authorityPathCovers(grantPath, requestedPath) {
|
|
14603
|
-
if (grantPath === "
|
|
14630
|
+
if (grantPath === "/") return true;
|
|
14604
14631
|
if (grantPath.endsWith("/")) return requestedPath.startsWith(grantPath);
|
|
14605
14632
|
return requestedPath === grantPath;
|
|
14606
14633
|
}
|
|
14607
14634
|
function requiresSecretsOptIn(space, prefix) {
|
|
14635
|
+
if (prefix === "/") return true;
|
|
14608
14636
|
if (space === "secrets" || space.endsWith(":secrets")) return true;
|
|
14609
14637
|
const firstSegment = prefix.split("/", 1)[0];
|
|
14610
14638
|
return firstSegment === "vault";
|
|
@@ -16709,6 +16737,9 @@ function parseUcanGrant(input) {
|
|
|
16709
16737
|
};
|
|
16710
16738
|
}
|
|
16711
16739
|
function kvPathIn(resource, space) {
|
|
16740
|
+
for (const root of [`${space}/kv`, `tinycloud://${space}/kv`]) {
|
|
16741
|
+
if (resource === root || resource === `${root}/`) return "/";
|
|
16742
|
+
}
|
|
16712
16743
|
for (const base of [`${space}/kv/`, `tinycloud://${space}/kv/`]) {
|
|
16713
16744
|
if (resource.startsWith(base)) return resource.slice(base.length);
|
|
16714
16745
|
}
|
|
@@ -16730,9 +16761,8 @@ function assertGrantInstallable(grant, input) {
|
|
|
16730
16761
|
`The grant was issued to ${grant.audience}, not this device (${input.deviceDid}).`
|
|
16731
16762
|
);
|
|
16732
16763
|
}
|
|
16733
|
-
const requiredPaths = input.prefix.endsWith("/") ? [input.prefix] : [input.prefix, `${input.prefix}/`];
|
|
16734
16764
|
for (const ability of ["tinycloud.kv/sync", "tinycloud.kv/get"]) {
|
|
16735
|
-
if (!
|
|
16765
|
+
if (!grantCovers(grant, input.space, input.prefix, ability)) {
|
|
16736
16766
|
throw new ReplicaError(
|
|
16737
16767
|
ReplicaErrorCode.GRANT_NOT_COVERING,
|
|
16738
16768
|
`The grant does not carry ${ability} on ${input.space}/kv/${input.prefix}.`,
|
|
@@ -16787,6 +16817,23 @@ function isPrincipalDid(value) {
|
|
|
16787
16817
|
}
|
|
16788
16818
|
|
|
16789
16819
|
// src/transport.ts
|
|
16820
|
+
var featureCache = /* @__PURE__ */ new Map();
|
|
16821
|
+
function featuresForHost(host, fetchFn) {
|
|
16822
|
+
const cached = featureCache.get(host);
|
|
16823
|
+
if (cached !== void 0) return cached;
|
|
16824
|
+
const discovered = fetchFn(`${host.replace(/\/+$/, "")}/info`).then(async (response) => {
|
|
16825
|
+
if (!response.ok) return void 0;
|
|
16826
|
+
const info = await response.json();
|
|
16827
|
+
if (typeof info !== "object" || info === null || !("features" in info)) return void 0;
|
|
16828
|
+
const features = info.features;
|
|
16829
|
+
return Array.isArray(features) && features.every((feature) => typeof feature === "string") ? features : void 0;
|
|
16830
|
+
}).catch(() => void 0);
|
|
16831
|
+
featureCache.set(host, discovered);
|
|
16832
|
+
void discovered.then((features) => {
|
|
16833
|
+
if (features === void 0) featureCache.delete(host);
|
|
16834
|
+
});
|
|
16835
|
+
return discovered;
|
|
16836
|
+
}
|
|
16790
16837
|
var REVOKED_CODES = /* @__PURE__ */ new Set(["AUTH_DELEGATION_REVOKED", "AUTH_DELEGATION_ANCESTOR_REVOKED"]);
|
|
16791
16838
|
var REVOKED_BODY2 = /^(?:Invalid invocation: )?(?:delegation-revoked: [A-Za-z0-9]+|delegation-ancestor-revoked: ancestor=[A-Za-z0-9]+ (?:invoked|parent)=[A-Za-z0-9]+)$/;
|
|
16792
16839
|
function isRevocation(error, status, request) {
|
|
@@ -16876,9 +16923,26 @@ function fetched(key, result, request) {
|
|
|
16876
16923
|
}
|
|
16877
16924
|
return { bytes: result.data.data, etag: result.data.headers.etag ?? "" };
|
|
16878
16925
|
}
|
|
16879
|
-
function kvSyncTransport(kv) {
|
|
16926
|
+
function kvSyncTransport(kv, options = {}) {
|
|
16927
|
+
const host = options.host ?? kv.host;
|
|
16928
|
+
const fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
16880
16929
|
return {
|
|
16881
16930
|
async syncPage({ prefix, cursor, limit, retentionGrant, signal }) {
|
|
16931
|
+
if (prefix === "/") {
|
|
16932
|
+
if (typeof host !== "string" || host.length === 0) {
|
|
16933
|
+
throw new ReplicaError(
|
|
16934
|
+
ReplicaErrorCode.INVALID_ARGUMENT,
|
|
16935
|
+
"Root sync requires a node host to check kv-sync-root; pass { host } to kvSyncTransport when the KV service does not expose its host."
|
|
16936
|
+
);
|
|
16937
|
+
}
|
|
16938
|
+
const features = await featuresForHost(host, fetchFn);
|
|
16939
|
+
if (features !== void 0 && !features.includes("kv-sync-root")) {
|
|
16940
|
+
throw new ReplicaError(
|
|
16941
|
+
ReplicaErrorCode.REPLICATION_ROOT_UNSUPPORTED,
|
|
16942
|
+
"This node doesn't support whole-space sync yet."
|
|
16943
|
+
);
|
|
16944
|
+
}
|
|
16945
|
+
}
|
|
16882
16946
|
const result = await kv.changes({
|
|
16883
16947
|
prefix,
|
|
16884
16948
|
limit,
|
|
@@ -16889,10 +16953,10 @@ function kvSyncTransport(kv) {
|
|
|
16889
16953
|
if (!result.ok) throw replicaErrorFromService(result.error, "Syncing");
|
|
16890
16954
|
return checkPage(result.data);
|
|
16891
16955
|
},
|
|
16892
|
-
async fetchContent(keys,
|
|
16956
|
+
async fetchContent(keys, options2) {
|
|
16893
16957
|
const contents = /* @__PURE__ */ new Map();
|
|
16894
16958
|
if (keys.length === 0) return contents;
|
|
16895
|
-
const signal =
|
|
16959
|
+
const signal = options2?.signal;
|
|
16896
16960
|
const ordered = [...keys].sort(compareKeyBytes);
|
|
16897
16961
|
const batch = await kv.batchGet(ordered, { binary: true, ...signal === void 0 ? {} : { signal } });
|
|
16898
16962
|
if (batch.ok) {
|
|
@@ -17165,6 +17229,7 @@ var IndexedDbReplicaStore = class _IndexedDbReplicaStore {
|
|
|
17165
17229
|
throw new ReplicaError(ReplicaErrorCode.RESET_REQUIRED, "The replica was purged or reopened; open it again to continue.");
|
|
17166
17230
|
}
|
|
17167
17231
|
if (state === void 0 || state.purged === true) return null;
|
|
17232
|
+
assertFolderPrefix2(state.config.prefix);
|
|
17168
17233
|
return {
|
|
17169
17234
|
config: state.config,
|
|
17170
17235
|
grant: state.grant,
|
|
@@ -17185,6 +17250,7 @@ var IndexedDbReplicaStore = class _IndexedDbReplicaStore {
|
|
|
17185
17250
|
};
|
|
17186
17251
|
}
|
|
17187
17252
|
async init(c) {
|
|
17253
|
+
assertFolderPrefix2(c.prefix);
|
|
17188
17254
|
const generation = await this.#writeTx(
|
|
17189
17255
|
[STORE_META],
|
|
17190
17256
|
"Creating the replica",
|
|
@@ -18014,7 +18080,7 @@ async function handleSync(request) {
|
|
|
18014
18080
|
if (state === null) throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, "The replica has not been created.");
|
|
18015
18081
|
const engine = new Replica({
|
|
18016
18082
|
store: s.store,
|
|
18017
|
-
transportFor: (grant) => kvSyncTransport(boundKv(grant, s)),
|
|
18083
|
+
transportFor: (grant) => kvSyncTransport(boundKv(grant, s), { host: s.host }),
|
|
18018
18084
|
// Siblings hear every committed page and repair batch, not only whole
|
|
18019
18085
|
// syncs: a sync that fails on page 2 still reports page 1.
|
|
18020
18086
|
onCommit: () => broadcastCommitted(s)
|