tokelytics 0.3.1 → 0.3.3
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 +1 -1
- package/bin/tokelytics.mjs +137 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ prompts or responses.
|
|
|
9
9
|
|
|
10
10
|
```
|
|
11
11
|
npx tokelytics@latest login # opens your browser to approve — no setup, no keys
|
|
12
|
-
npx tokelytics@latest watch #
|
|
12
|
+
npx tokelytics@latest watch # local live updates; cloud backup every 20m
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Keep `@latest` in install and onboarding commands. Agents older than 0.3.1
|
package/bin/tokelytics.mjs
CHANGED
|
@@ -1781,7 +1781,7 @@ var FirestoreSink = class {
|
|
|
1781
1781
|
};
|
|
1782
1782
|
|
|
1783
1783
|
// src/version.ts
|
|
1784
|
-
var AGENT_VERSION = "0.3.
|
|
1784
|
+
var AGENT_VERSION = "0.3.3";
|
|
1785
1785
|
|
|
1786
1786
|
// src/sync.ts
|
|
1787
1787
|
async function runSync(connectors, sink, state, device, limitCollectors = [], options = {}) {
|
|
@@ -1863,7 +1863,7 @@ async function runSync(connectors, sink, state, device, limitCollectors = [], op
|
|
|
1863
1863
|
lastLocalEventAt: publication.lastLocalEventAt,
|
|
1864
1864
|
lastCloudWriteAt: publication.lastCloudWriteAt
|
|
1865
1865
|
};
|
|
1866
|
-
if (publication.dashboardDirty && cloudWriteDue && hasBudget) {
|
|
1866
|
+
if (publication.dashboardDirty && cloudWriteDue && hasBudget && !options.suppressCloudPublish) {
|
|
1867
1867
|
try {
|
|
1868
1868
|
dashboard.sync = {
|
|
1869
1869
|
...dashboard.sync,
|
|
@@ -1877,7 +1877,12 @@ async function runSync(connectors, sink, state, device, limitCollectors = [], op
|
|
|
1877
1877
|
publication.dashboardDirty = false;
|
|
1878
1878
|
published = true;
|
|
1879
1879
|
} catch (error) {
|
|
1880
|
-
dashboard.sync
|
|
1880
|
+
dashboard.sync = {
|
|
1881
|
+
...dashboard.sync,
|
|
1882
|
+
cloudWritesToday: publication.cloudWritesToday ?? 0,
|
|
1883
|
+
localChangesPending: true,
|
|
1884
|
+
lastCloudWriteAt: publication.lastCloudWriteAt
|
|
1885
|
+
};
|
|
1881
1886
|
publicationError = error;
|
|
1882
1887
|
}
|
|
1883
1888
|
}
|
|
@@ -1943,10 +1948,15 @@ async function createRunner() {
|
|
|
1943
1948
|
});
|
|
1944
1949
|
const sink = new FirestoreSink(rest, session.uid);
|
|
1945
1950
|
return {
|
|
1951
|
+
uid: session.uid,
|
|
1946
1952
|
connectors,
|
|
1947
1953
|
watchPaths() {
|
|
1948
1954
|
return connectors.flatMap((c) => c.watchPaths());
|
|
1949
1955
|
},
|
|
1956
|
+
async localSnapshot() {
|
|
1957
|
+
const state = await loadState();
|
|
1958
|
+
return state.dashboard ? prepareDashboardSnapshotForCloud(state.dashboard) : void 0;
|
|
1959
|
+
},
|
|
1950
1960
|
async syncOnce(options = {}) {
|
|
1951
1961
|
const state = await loadState();
|
|
1952
1962
|
const device = {
|
|
@@ -1968,13 +1978,13 @@ async function createRunner() {
|
|
|
1968
1978
|
publication: res.publication
|
|
1969
1979
|
};
|
|
1970
1980
|
if (!sameState(state, nextState)) await saveState(nextState);
|
|
1971
|
-
if (res.publicationError) throw res.publicationError;
|
|
1972
1981
|
return {
|
|
1973
1982
|
processedTurns: res.processedTurns,
|
|
1974
1983
|
processedLimits: res.processedLimits,
|
|
1975
1984
|
byProvider: res.byProvider,
|
|
1976
1985
|
published: res.published,
|
|
1977
|
-
cloudWritesToday: res.publication.cloudWritesToday ?? 0
|
|
1986
|
+
cloudWritesToday: res.publication.cloudWritesToday ?? 0,
|
|
1987
|
+
publicationError: res.publicationError
|
|
1978
1988
|
};
|
|
1979
1989
|
}
|
|
1980
1990
|
};
|
|
@@ -3690,16 +3700,99 @@ function watch(paths, options = {}) {
|
|
|
3690
3700
|
}
|
|
3691
3701
|
var esm_default = { watch, FSWatcher };
|
|
3692
3702
|
|
|
3703
|
+
// src/local-server.ts
|
|
3704
|
+
import { createServer as createServer2 } from "node:http";
|
|
3705
|
+
var LOCAL_DASHBOARD_PORT = 47821;
|
|
3706
|
+
var ALLOWED_ORIGINS = /* @__PURE__ */ new Set([
|
|
3707
|
+
"https://tokelytics.web.app",
|
|
3708
|
+
"https://tokelytics.firebaseapp.com",
|
|
3709
|
+
"http://localhost:3000",
|
|
3710
|
+
"http://127.0.0.1:3000"
|
|
3711
|
+
]);
|
|
3712
|
+
async function startLocalDashboardServer(getSnapshot, ownerUid, port = LOCAL_DASHBOARD_PORT) {
|
|
3713
|
+
const server = createServer2(async (request, response) => {
|
|
3714
|
+
const origin = request.headers.origin;
|
|
3715
|
+
if (!origin || !ALLOWED_ORIGINS.has(origin)) {
|
|
3716
|
+
response.writeHead(403, { "Content-Type": "application/json" });
|
|
3717
|
+
response.end(JSON.stringify({ error: "Origin is not allowed." }));
|
|
3718
|
+
return;
|
|
3719
|
+
}
|
|
3720
|
+
const corsHeaders = {
|
|
3721
|
+
"Access-Control-Allow-Origin": origin,
|
|
3722
|
+
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
|
3723
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
3724
|
+
"Access-Control-Allow-Private-Network": "true",
|
|
3725
|
+
"Cache-Control": "no-store",
|
|
3726
|
+
Vary: "Origin, Access-Control-Request-Private-Network"
|
|
3727
|
+
};
|
|
3728
|
+
if (request.method === "OPTIONS") {
|
|
3729
|
+
response.writeHead(204, corsHeaders);
|
|
3730
|
+
response.end();
|
|
3731
|
+
return;
|
|
3732
|
+
}
|
|
3733
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
3734
|
+
if (request.method !== "GET" || url.pathname !== "/v1/snapshot" || url.searchParams.get("uid") !== ownerUid) {
|
|
3735
|
+
response.writeHead(404, { ...corsHeaders, "Content-Type": "application/json" });
|
|
3736
|
+
response.end(JSON.stringify({ error: "Not found." }));
|
|
3737
|
+
return;
|
|
3738
|
+
}
|
|
3739
|
+
try {
|
|
3740
|
+
const snapshot2 = await getSnapshot();
|
|
3741
|
+
response.writeHead(200, { ...corsHeaders, "Content-Type": "application/json" });
|
|
3742
|
+
response.end(
|
|
3743
|
+
JSON.stringify({
|
|
3744
|
+
service: "tokelytics",
|
|
3745
|
+
version: 1,
|
|
3746
|
+
servedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3747
|
+
snapshot: snapshot2 ?? null
|
|
3748
|
+
})
|
|
3749
|
+
);
|
|
3750
|
+
} catch {
|
|
3751
|
+
response.writeHead(503, { ...corsHeaders, "Content-Type": "application/json" });
|
|
3752
|
+
response.end(JSON.stringify({ error: "Local snapshot is temporarily unavailable." }));
|
|
3753
|
+
}
|
|
3754
|
+
});
|
|
3755
|
+
await listen(server, port);
|
|
3756
|
+
const address = server.address();
|
|
3757
|
+
const resolvedPort = typeof address === "object" && address ? address.port : port;
|
|
3758
|
+
return {
|
|
3759
|
+
port: resolvedPort,
|
|
3760
|
+
close: () => close(server)
|
|
3761
|
+
};
|
|
3762
|
+
}
|
|
3763
|
+
function listen(server, port) {
|
|
3764
|
+
return new Promise((resolve4, reject) => {
|
|
3765
|
+
const onError = (error) => reject(error);
|
|
3766
|
+
server.once("error", onError);
|
|
3767
|
+
server.listen(port, "127.0.0.1", () => {
|
|
3768
|
+
server.off("error", onError);
|
|
3769
|
+
resolve4();
|
|
3770
|
+
});
|
|
3771
|
+
});
|
|
3772
|
+
}
|
|
3773
|
+
function close(server) {
|
|
3774
|
+
return new Promise((resolve4, reject) => {
|
|
3775
|
+
server.close((error) => error ? reject(error) : resolve4());
|
|
3776
|
+
});
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3693
3779
|
// src/watch.ts
|
|
3694
3780
|
var DEBOUNCE_MS = 1200;
|
|
3695
3781
|
var LIMIT_REFRESH_MS = 6e4;
|
|
3696
3782
|
var FALLBACK_SCAN_MS = 5e3;
|
|
3697
3783
|
var DEVICE_HEARTBEAT_MS = 60 * 6e4;
|
|
3698
|
-
var CLOUD_SYNC_INTERVAL_MS =
|
|
3699
|
-
var MAX_CLOUD_WRITES_PER_DAY =
|
|
3784
|
+
var CLOUD_SYNC_INTERVAL_MS = 20 * 6e4;
|
|
3785
|
+
var MAX_CLOUD_WRITES_PER_DAY = 24;
|
|
3700
3786
|
var QUOTA_RETRY_MS = 15 * 6e4;
|
|
3701
3787
|
var QUOTA_RETRY_JITTER_MS = 5 * 6e4;
|
|
3702
3788
|
async function watch2(runner) {
|
|
3789
|
+
let localServer;
|
|
3790
|
+
try {
|
|
3791
|
+
localServer = await startLocalDashboardServer(() => runner.localSnapshot(), runner.uid);
|
|
3792
|
+
console.log(`Local live dashboard ready on 127.0.0.1:${localServer.port}.`);
|
|
3793
|
+
} catch (error) {
|
|
3794
|
+
console.warn(`Local live dashboard unavailable: ${error.message}`);
|
|
3795
|
+
}
|
|
3703
3796
|
const startupRetryAfter = await safeSync(runner, {
|
|
3704
3797
|
reason: "startup",
|
|
3705
3798
|
refreshLimits: true,
|
|
@@ -3707,7 +3800,7 @@ async function watch2(runner) {
|
|
|
3707
3800
|
deviceHeartbeatMs: DEVICE_HEARTBEAT_MS,
|
|
3708
3801
|
cloudSyncIntervalMs: CLOUD_SYNC_INTERVAL_MS,
|
|
3709
3802
|
maxCloudWritesPerDay: MAX_CLOUD_WRITES_PER_DAY
|
|
3710
|
-
});
|
|
3803
|
+
}, false);
|
|
3711
3804
|
const paths = runner.watchPaths();
|
|
3712
3805
|
const watcher = esm_default.watch(paths, {
|
|
3713
3806
|
ignoreInitial: true,
|
|
@@ -3720,11 +3813,11 @@ async function watch2(runner) {
|
|
|
3720
3813
|
let pending;
|
|
3721
3814
|
let running = false;
|
|
3722
3815
|
let scheduledAt = 0;
|
|
3723
|
-
let
|
|
3816
|
+
let cloudBlockedUntil = startupRetryAfter ? Date.now() + startupRetryAfter : 0;
|
|
3724
3817
|
const schedule = (request, delay = DEBOUNCE_MS) => {
|
|
3725
3818
|
pending = mergeRequests(pending, request);
|
|
3726
3819
|
if (running) return;
|
|
3727
|
-
const target =
|
|
3820
|
+
const target = Date.now() + delay;
|
|
3728
3821
|
if (timer && scheduledAt <= target) return;
|
|
3729
3822
|
if (timer) clearTimeout(timer);
|
|
3730
3823
|
scheduledAt = target;
|
|
@@ -3735,13 +3828,12 @@ async function watch2(runner) {
|
|
|
3735
3828
|
pending = void 0;
|
|
3736
3829
|
if (!next) return;
|
|
3737
3830
|
running = true;
|
|
3738
|
-
const retryAfterMs = await safeSync(runner, next);
|
|
3831
|
+
const retryAfterMs = await safeSync(runner, next, Date.now() < cloudBlockedUntil);
|
|
3739
3832
|
running = false;
|
|
3740
3833
|
if (retryAfterMs) {
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
blockedUntil = 0;
|
|
3834
|
+
cloudBlockedUntil = Date.now() + retryAfterMs;
|
|
3835
|
+
} else if (Date.now() >= cloudBlockedUntil) {
|
|
3836
|
+
cloudBlockedUntil = 0;
|
|
3745
3837
|
}
|
|
3746
3838
|
if (pending) {
|
|
3747
3839
|
const queued = pending;
|
|
@@ -3776,43 +3868,53 @@ async function watch2(runner) {
|
|
|
3776
3868
|
${paths.join("\n ")}
|
|
3777
3869
|
Press Ctrl+C to stop.`);
|
|
3778
3870
|
await new Promise((resolve4) => {
|
|
3779
|
-
|
|
3871
|
+
const stop = () => {
|
|
3780
3872
|
clearInterval(refreshTimer);
|
|
3781
3873
|
clearInterval(fallbackTimer);
|
|
3782
3874
|
if (timer) clearTimeout(timer);
|
|
3783
|
-
void watcher.close().then(resolve4);
|
|
3784
|
-
}
|
|
3875
|
+
void Promise.all([watcher.close(), localServer?.close()]).then(() => resolve4());
|
|
3876
|
+
};
|
|
3877
|
+
process.once("SIGINT", stop);
|
|
3878
|
+
process.once("SIGTERM", stop);
|
|
3785
3879
|
});
|
|
3786
3880
|
}
|
|
3787
|
-
async function safeSync(runner, request) {
|
|
3881
|
+
async function safeSync(runner, request, suppressCloudPublish) {
|
|
3788
3882
|
try {
|
|
3789
3883
|
const { reason, ...options } = request;
|
|
3790
|
-
const { processedTurns, byProvider, published, cloudWritesToday } = await runner.syncOnce(
|
|
3884
|
+
const { processedTurns, byProvider, published, cloudWritesToday, publicationError } = await runner.syncOnce({
|
|
3885
|
+
...options,
|
|
3886
|
+
suppressCloudPublish
|
|
3887
|
+
});
|
|
3791
3888
|
if (processedTurns > 0) {
|
|
3792
3889
|
const detail = Object.entries(byProvider).map(([p, n]) => `${p}:${n}`).join(" ");
|
|
3793
3890
|
console.log(
|
|
3794
|
-
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] processed ${processedTurns} turn(s) locally (${detail}); ` + (published ? `cloud snapshot published (${cloudWritesToday}
|
|
3891
|
+
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] processed ${processedTurns} turn(s) locally (${detail}); ` + (published ? `cloud snapshot published (${cloudWritesToday}/${MAX_CLOUD_WRITES_PER_DAY} today)` : "cloud backup queued")
|
|
3795
3892
|
);
|
|
3796
3893
|
} else if (reason === "startup") {
|
|
3797
3894
|
console.log(
|
|
3798
|
-
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] local snapshot ready; ` + (published ? `cloud snapshot published (${cloudWritesToday}
|
|
3895
|
+
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] local snapshot ready; ` + (published ? `cloud snapshot published (${cloudWritesToday}/${MAX_CLOUD_WRITES_PER_DAY} today)` : "cloud backup queued")
|
|
3799
3896
|
);
|
|
3800
3897
|
}
|
|
3898
|
+
if (publicationError) return cloudRetryDelay(publicationError);
|
|
3801
3899
|
} catch (err) {
|
|
3802
|
-
|
|
3803
|
-
const retryAfter = Math.max(
|
|
3804
|
-
err.retryAfterMs ?? 0,
|
|
3805
|
-
QUOTA_RETRY_MS + Math.floor(Math.random() * QUOTA_RETRY_JITTER_MS)
|
|
3806
|
-
);
|
|
3807
|
-
console.error(
|
|
3808
|
-
`Cloud sync paused: Firestore quota is exhausted; the local snapshot is safe. Retrying in ${Math.ceil(retryAfter / 6e4)}m.`
|
|
3809
|
-
);
|
|
3810
|
-
return retryAfter;
|
|
3811
|
-
}
|
|
3812
|
-
console.error(`Sync error: ${err.message}`);
|
|
3900
|
+
console.error(`Local sync error: ${err.message}`);
|
|
3813
3901
|
return 15e3;
|
|
3814
3902
|
}
|
|
3815
3903
|
}
|
|
3904
|
+
function cloudRetryDelay(error) {
|
|
3905
|
+
if (error instanceof FirestoreRestError && error.status === 429) {
|
|
3906
|
+
const retryAfter = Math.max(
|
|
3907
|
+
error.retryAfterMs ?? 0,
|
|
3908
|
+
QUOTA_RETRY_MS + Math.floor(Math.random() * QUOTA_RETRY_JITTER_MS)
|
|
3909
|
+
);
|
|
3910
|
+
console.error(
|
|
3911
|
+
`Cloud backup paused: Firestore quota is exhausted; local live updates remain active. Retrying in ${Math.ceil(retryAfter / 6e4)}m.`
|
|
3912
|
+
);
|
|
3913
|
+
return retryAfter;
|
|
3914
|
+
}
|
|
3915
|
+
console.error(`Cloud backup error: ${error.message}. Local live updates remain active; retrying shortly.`);
|
|
3916
|
+
return 6e4;
|
|
3917
|
+
}
|
|
3816
3918
|
function mergeRequests(current, incoming) {
|
|
3817
3919
|
if (!current) return incoming;
|
|
3818
3920
|
return {
|
|
@@ -3902,7 +4004,8 @@ async function main(argv) {
|
|
|
3902
4004
|
const lock = await acquireWatchLock();
|
|
3903
4005
|
try {
|
|
3904
4006
|
const runner = await createRunner();
|
|
3905
|
-
const { processedTurns, processedLimits, byProvider } = await runner.syncOnce();
|
|
4007
|
+
const { processedTurns, processedLimits, byProvider, publicationError } = await runner.syncOnce();
|
|
4008
|
+
if (publicationError) throw publicationError;
|
|
3906
4009
|
const detail = Object.entries(byProvider).map(([p, n]) => `${p}:${n}`).join(" ") || "none";
|
|
3907
4010
|
console.log(`Processed ${processedTurns} turn(s) (${detail}); refreshed ${processedLimits} usage provider(s).`);
|
|
3908
4011
|
} finally {
|