@peers-app/peers-sdk 0.19.6 → 0.19.8

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.
@@ -33,16 +33,35 @@ export declare const consoleLogSchema: z.ZodObject<{
33
33
  stackTrace?: string | undefined;
34
34
  }>;
35
35
  export type IConsoleLog = z.infer<typeof consoleLogSchema>;
36
+ /** Cutoff within this many ms of `Date.now()` is treated as "clear all" (manual clear). */
37
+ export declare const CLEAR_ALL_CUTOFF_SLACK_MS = 5000;
38
+ /**
39
+ * Returns whether a cutoff timestamp should trigger a full-table clear (drop + recreate)
40
+ * rather than a conditional delete. Used when the UI calls `deleteOldLogs(Date.now())`.
41
+ * @param cutoffTimestamp - Millisecond cutoff passed to `deleteOldLogs`
42
+ */
43
+ export declare function isClearAllCutoff(cutoffTimestamp: number): boolean;
44
+ /**
45
+ * System table for cross-process console log storage with automatic TTL cleanup.
46
+ */
36
47
  export declare class ConsoleLogsTable extends Table<IConsoleLog> {
37
48
  readonly LOG_TTL: number;
38
49
  readonly LOG_CLEANUP_INTERVAL: number;
39
50
  constructor(metaData: ITableMetaData, deps: ITableDependencies);
40
51
  private logCleanupInitialized;
52
+ /** Schedules startup and daily cleanup of logs older than {@link LOG_TTL}. */
41
53
  initializeLogCleanup(): Promise<void>;
54
+ private resolveSqlDataSource;
55
+ /**
56
+ * Remove console logs older than `cutoffTimestamp`.
57
+ *
58
+ * When the cutoff is near `Date.now()` (within {@link CLEAR_ALL_CUTOFF_SLACK_MS}), uses
59
+ * drop + recreate for a fast full clear. Otherwise deletes rows with `timestamp < cutoff`.
60
+ * @param cutoffTimestamp - Defaults to seven days ago for scheduled TTL cleanup
61
+ */
42
62
  deleteOldLogs(cutoffTimestamp?: number): Promise<void>;
43
63
  }
44
64
  /**
45
- * We always use the user's personal data context so all logs are written to the same place. This requires
46
- * that the table be accessed in an asynchronous way
65
+ * Returns the user's personal {@link ConsoleLogsTable} (logs are not synced across devices).
47
66
  */
48
67
  export declare function ConsoleLogs(): Promise<ConsoleLogsTable>;
@@ -34,7 +34,8 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
34
34
  done = true;
35
35
  };
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
- exports.ConsoleLogsTable = exports.consoleLogSchema = void 0;
37
+ exports.ConsoleLogsTable = exports.CLEAR_ALL_CUTOFF_SLACK_MS = exports.consoleLogSchema = void 0;
38
+ exports.isClearAllCutoff = isClearAllCutoff;
38
39
  exports.ConsoleLogs = ConsoleLogs;
39
40
  const zod_1 = require("zod");
40
41
  const user_context_singleton_1 = require("../context/user-context-singleton");
@@ -57,6 +58,8 @@ exports.consoleLogSchema = zod_1.z.object({
57
58
  context: zod_types_1.zodAnyObjectOrArray.optional().describe("Additional structured context data"),
58
59
  stackTrace: zod_1.z.string().optional().describe("Stack trace for errors"),
59
60
  });
61
+ /** Cutoff within this many ms of `Date.now()` is treated as "clear all" (manual clear). */
62
+ exports.CLEAR_ALL_CUTOFF_SLACK_MS = 5000;
60
63
  const metaData = {
61
64
  name: "ConsoleLogs",
62
65
  description: "System-wide console log entries with cross-process visibility.",
@@ -70,6 +73,17 @@ const metaData = {
70
73
  { fields: ["processInstanceId"] },
71
74
  ],
72
75
  };
76
+ /**
77
+ * Returns whether a cutoff timestamp should trigger a full-table clear (drop + recreate)
78
+ * rather than a conditional delete. Used when the UI calls `deleteOldLogs(Date.now())`.
79
+ * @param cutoffTimestamp - Millisecond cutoff passed to `deleteOldLogs`
80
+ */
81
+ function isClearAllCutoff(cutoffTimestamp) {
82
+ return cutoffTimestamp >= Date.now() - exports.CLEAR_ALL_CUTOFF_SLACK_MS;
83
+ }
84
+ /**
85
+ * System table for cross-process console log storage with automatic TTL cleanup.
86
+ */
73
87
  let ConsoleLogsTable = (() => {
74
88
  let _classSuper = orm_1.Table;
75
89
  let _instanceExtraInitializers = [];
@@ -93,6 +107,7 @@ let ConsoleLogsTable = (() => {
93
107
  }
94
108
  }
95
109
  logCleanupInitialized = false;
110
+ /** Schedules startup and daily cleanup of logs older than {@link LOG_TTL}. */
96
111
  async initializeLogCleanup() {
97
112
  if (this.logCleanupInitialized) {
98
113
  return;
@@ -104,30 +119,57 @@ let ConsoleLogsTable = (() => {
104
119
  // TODO - consider doing this as a workflow
105
120
  setInterval(() => this.deleteOldLogs(), this.LOG_CLEANUP_INTERVAL);
106
121
  }
107
- async deleteOldLogs(cutoffTimestamp = Date.now() - 7 * 24 * 60 * 60 * 1000) {
122
+ resolveSqlDataSource() {
123
+ let ds = this.dataSource;
124
+ while (!(ds instanceof orm_1.SQLDataSource) && ds.dataSource) {
125
+ ds = ds.dataSource;
126
+ }
127
+ return ds instanceof orm_1.SQLDataSource ? ds : undefined;
128
+ }
129
+ /**
130
+ * Remove console logs older than `cutoffTimestamp`.
131
+ *
132
+ * When the cutoff is near `Date.now()` (within {@link CLEAR_ALL_CUTOFF_SLACK_MS}), uses
133
+ * drop + recreate for a fast full clear. Otherwise deletes rows with `timestamp < cutoff`.
134
+ * @param cutoffTimestamp - Defaults to seven days ago for scheduled TTL cleanup
135
+ */
136
+ async deleteOldLogs(cutoffTimestamp = Date.now() - 24 * 60 * 60 * 1000) {
137
+ if (isClearAllCutoff(cutoffTimestamp)) {
138
+ const sqlDs = this.resolveSqlDataSource();
139
+ if (sqlDs) {
140
+ console.log("clearing all console logs via drop+recreate");
141
+ await sqlDs.dropTableIfExists();
142
+ await sqlDs.initTable(true);
143
+ console.log("cleared all console logs");
144
+ return;
145
+ }
146
+ if (typeof this.dataSource.clearTable === "function") {
147
+ await this.dataSource.clearTable();
148
+ console.log("cleared all console logs via clearTable");
149
+ return;
150
+ }
151
+ console.warn("clear-all requested but no SQL data source available");
152
+ return;
153
+ }
108
154
  const count = await this.count({ timestamp: { $lt: cutoffTimestamp } });
109
155
  if (count === 0) {
110
156
  console.log(`No old logs to clean up older than ${new Date(cutoffTimestamp).toISOString()}`);
111
157
  return;
112
158
  }
113
- let ds = this.dataSource;
114
- while (!(ds instanceof orm_1.SQLDataSource) && ds.dataSource) {
115
- ds = ds.dataSource;
116
- }
117
- if (ds instanceof orm_1.SQLDataSource) {
118
- const db = ds.db;
119
- const result = await db.exec(`delete from ConsoleLogs where timestamp < $timestamp`, {
120
- timestamp: cutoffTimestamp,
121
- });
159
+ const sqlDs = this.resolveSqlDataSource();
160
+ if (sqlDs) {
161
+ const db = sqlDs.db;
162
+ const result = await db.exec(`DELETE FROM "${sqlDs.tableName}" WHERE "timestamp" < $timestamp`, { timestamp: cutoffTimestamp });
122
163
  console.log(`bulk deleted ${count} logs, changes: ${result.changes}`);
123
164
  }
124
165
  else {
125
166
  const cursor = this.cursor({ timestamp: { $lt: cutoffTimestamp } }, { sortBy: ["-timestamp"] });
126
- const count2 = 0;
167
+ let deleted = 0;
127
168
  for await (const log of cursor) {
128
- this.delete(log.logId);
169
+ await this.delete(log.logId);
170
+ deleted++;
129
171
  }
130
- console.log(`cursor deleted ${count2} logs`);
172
+ console.log(`cursor deleted ${deleted} logs`);
131
173
  }
132
174
  console.log(`cleaned up console logs`, { expectedCount: count });
133
175
  }
@@ -136,8 +178,7 @@ let ConsoleLogsTable = (() => {
136
178
  exports.ConsoleLogsTable = ConsoleLogsTable;
137
179
  (0, table_definitions_system_1.registerSystemTableDefinition)(metaData, exports.consoleLogSchema, ConsoleLogsTable);
138
180
  /**
139
- * We always use the user's personal data context so all logs are written to the same place. This requires
140
- * that the table be accessed in an asynchronous way
181
+ * Returns the user's personal {@link ConsoleLogsTable} (logs are not synced across devices).
141
182
  */
142
183
  async function ConsoleLogs() {
143
184
  const userContext = await (0, user_context_singleton_1.getUserContext)();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const SQLiteDB = require("better-sqlite3");
4
+ const event_registry_1 = require("../data/orm/event-registry");
5
+ const sql_data_source_1 = require("../data/orm/sql.data-source");
6
+ const types_1 = require("../data/orm/types");
7
+ const utils_1 = require("../utils");
8
+ const console_logs_table_1 = require("./console-logs.table");
9
+ class DBHarness {
10
+ _db = null;
11
+ get db() {
12
+ if (!this._db) {
13
+ this._db = new SQLiteDB(":memory:");
14
+ this._db.pragma("journal_mode = WAL");
15
+ }
16
+ return this._db;
17
+ }
18
+ async get(sql, params = []) {
19
+ return this.db.prepare(sql).get(params);
20
+ }
21
+ async all(sql, params = []) {
22
+ return this.db.prepare(sql).all(params);
23
+ }
24
+ async exec(sql, params = []) {
25
+ const result = this.db.prepare(sql).run(params);
26
+ return { changes: result.changes };
27
+ }
28
+ async close() {
29
+ this._db?.close();
30
+ this._db = null;
31
+ }
32
+ }
33
+ const consoleLogsMetaData = {
34
+ name: "ConsoleLogs",
35
+ description: "System-wide console log entries with cross-process visibility.",
36
+ primaryKeyName: "logId",
37
+ fields: (0, types_1.schemaToFields)(console_logs_table_1.consoleLogSchema),
38
+ localOnly: true,
39
+ indexes: [
40
+ { fields: ["timestamp"] },
41
+ { fields: ["level"] },
42
+ { fields: ["process"] },
43
+ { fields: ["processInstanceId"] },
44
+ ],
45
+ };
46
+ function createConsoleLogsTable(db) {
47
+ const sqlDs = new sql_data_source_1.SQLDataSource(db, consoleLogsMetaData, console_logs_table_1.consoleLogSchema);
48
+ const eventRegistry = new event_registry_1.EventRegistry("test-context");
49
+ const deps = {
50
+ dataSource: sqlDs,
51
+ eventRegistry,
52
+ schema: console_logs_table_1.consoleLogSchema,
53
+ };
54
+ return { table: new console_logs_table_1.ConsoleLogsTable(consoleLogsMetaData, deps), sqlDs };
55
+ }
56
+ describe("isClearAllCutoff", () => {
57
+ it("returns true when cutoff is now", () => {
58
+ expect((0, console_logs_table_1.isClearAllCutoff)(Date.now())).toBe(true);
59
+ });
60
+ it("returns true when cutoff is within slack window", () => {
61
+ expect((0, console_logs_table_1.isClearAllCutoff)(Date.now() - console_logs_table_1.CLEAR_ALL_CUTOFF_SLACK_MS + 1)).toBe(true);
62
+ });
63
+ it("returns false when cutoff is older than slack window", () => {
64
+ expect((0, console_logs_table_1.isClearAllCutoff)(Date.now() - console_logs_table_1.CLEAR_ALL_CUTOFF_SLACK_MS - 1)).toBe(false);
65
+ expect((0, console_logs_table_1.isClearAllCutoff)(Date.now() - 7 * 24 * 60 * 60 * 1000)).toBe(false);
66
+ });
67
+ });
68
+ describe("ConsoleLogsTable.deleteOldLogs", () => {
69
+ const db = new DBHarness();
70
+ afterAll(async () => {
71
+ await db.close();
72
+ });
73
+ beforeEach(async () => {
74
+ const { sqlDs } = createConsoleLogsTable(db);
75
+ await sqlDs.dropTableIfExists();
76
+ await sqlDs.initTable(true);
77
+ });
78
+ it("uses drop+recreate for clear-all cutoff without calling count", async () => {
79
+ const { table, sqlDs } = createConsoleLogsTable(db);
80
+ await sqlDs.initTable(true);
81
+ const now = Date.now();
82
+ await table.insert({
83
+ logId: (0, utils_1.newid)(),
84
+ timestamp: now - 1000,
85
+ level: "info",
86
+ process: "test",
87
+ processInstanceId: (0, utils_1.newid)(),
88
+ message: "old log",
89
+ });
90
+ const countSpy = jest.spyOn(table, "count");
91
+ const dropSpy = jest.spyOn(sqlDs, "dropTableIfExists");
92
+ const initSpy = jest.spyOn(sqlDs, "initTable");
93
+ await table.deleteOldLogs(Date.now());
94
+ expect(countSpy).not.toHaveBeenCalled();
95
+ expect(dropSpy).toHaveBeenCalled();
96
+ expect(initSpy).toHaveBeenCalledWith(true);
97
+ const remaining = await table.count();
98
+ expect(remaining).toBe(0);
99
+ countSpy.mockRestore();
100
+ dropSpy.mockRestore();
101
+ initSpy.mockRestore();
102
+ });
103
+ it("uses conditional delete for TTL cutoff and calls count", async () => {
104
+ const { table, sqlDs } = createConsoleLogsTable(db);
105
+ await sqlDs.initTable(true);
106
+ const now = Date.now();
107
+ const oldCutoff = now - 7 * 24 * 60 * 60 * 1000;
108
+ await table.insert({
109
+ logId: (0, utils_1.newid)(),
110
+ timestamp: oldCutoff - 1000,
111
+ level: "info",
112
+ process: "test",
113
+ processInstanceId: (0, utils_1.newid)(),
114
+ message: "expired log",
115
+ });
116
+ await table.insert({
117
+ logId: (0, utils_1.newid)(),
118
+ timestamp: now,
119
+ level: "info",
120
+ process: "test",
121
+ processInstanceId: (0, utils_1.newid)(),
122
+ message: "recent log",
123
+ });
124
+ const countSpy = jest.spyOn(table, "count");
125
+ const dropSpy = jest.spyOn(sqlDs, "dropTableIfExists");
126
+ const execSpy = jest.spyOn(sqlDs.db, "exec");
127
+ await table.deleteOldLogs(oldCutoff);
128
+ expect(countSpy).toHaveBeenCalledWith({ timestamp: { $lt: oldCutoff } });
129
+ expect(dropSpy).not.toHaveBeenCalled();
130
+ expect(execSpy).toHaveBeenCalledWith(`DELETE FROM "${sqlDs.tableName}" WHERE "timestamp" < $timestamp`, { timestamp: oldCutoff });
131
+ const remaining = await table.count();
132
+ expect(remaining).toBe(1);
133
+ countSpy.mockRestore();
134
+ dropSpy.mockRestore();
135
+ execSpy.mockRestore();
136
+ });
137
+ it("does nothing on partial delete when no rows match cutoff", async () => {
138
+ const { table, sqlDs } = createConsoleLogsTable(db);
139
+ await sqlDs.initTable(true);
140
+ const now = Date.now();
141
+ await table.insert({
142
+ logId: (0, utils_1.newid)(),
143
+ timestamp: now,
144
+ level: "info",
145
+ process: "test",
146
+ processInstanceId: (0, utils_1.newid)(),
147
+ message: "recent log",
148
+ });
149
+ const execSpy = jest.spyOn(sqlDs.db, "exec");
150
+ const oldCutoff = now - 7 * 24 * 60 * 60 * 1000;
151
+ await table.deleteOldLogs(oldCutoff);
152
+ expect(execSpy).not.toHaveBeenCalled();
153
+ expect(await table.count()).toBe(1);
154
+ execSpy.mockRestore();
155
+ });
156
+ });
@@ -126,7 +126,10 @@ async function installPvInGroupNoActivate(source, dest, pv) {
126
126
  await copyFileTree(source, dest, file);
127
127
  }
128
128
  const propagatedPv = buildPropagatedPv(pv, files);
129
- await (0, package_versions_1.PackageVersions)(dest).signAndSave(propagatedPv, { saveAsSnapshot: true });
129
+ await (0, package_versions_1.PackageVersions)(dest).signAndSave(propagatedPv, {
130
+ saveAsSnapshot: true,
131
+ restoreIfDeleted: true,
132
+ });
130
133
  return true;
131
134
  }
132
135
  async function installPvInGroup(source, dest, pv, retryCnt = 0) {
@@ -164,7 +167,10 @@ async function installPvInGroup(source, dest, pv, retryCnt = 0) {
164
167
  }
165
168
  // Save sanitized PV
166
169
  const propagatedPv = buildPropagatedPv(pv, files);
167
- await (0, package_versions_1.PackageVersions)(dest).signAndSave(propagatedPv, { saveAsSnapshot: true });
170
+ await (0, package_versions_1.PackageVersions)(dest).signAndSave(propagatedPv, {
171
+ saveAsSnapshot: true,
172
+ restoreIfDeleted: true,
173
+ });
168
174
  // Activation pass
169
175
  const activation = await withActivationLock(dest, pv.packageId, () => activateBestEligibleVersion(dest, pkg));
170
176
  return makeResult(dest, pv, activation.activated && activation.activePackageVersionId === pv.packageVersionId);
@@ -431,7 +431,7 @@ describe("discoverAndPropagateVersions", () => {
431
431
  });
432
432
  expect(mockPvSignAndSave).toHaveBeenCalledWith(expect.objectContaining({
433
433
  packageVersionId: olderSignedPv.packageVersionId,
434
- }), { saveAsSnapshot: true });
434
+ }), { saveAsSnapshot: true, restoreIfDeleted: true });
435
435
  });
436
436
  it("installs beta PVs in stable-only groups without activating them", async () => {
437
437
  const groupA = "group-a";
@@ -884,7 +884,7 @@ describe("discoverAndPropagateVersions", () => {
884
884
  await (0, package_propagation_1.discoverAndPropagateVersions)(userContext);
885
885
  expect(mockPvSignAndSave).toHaveBeenCalledWith(expect.objectContaining({
886
886
  packageVersionHash: (0, package_versions_1.computePackageVersionHash)(signedPv.version, signedPv.versionTag ?? "", signedPv.packageBundleFileHash, signedPv.routesBundleFileHash, signedPv.uiBundleFileHash),
887
- }), { saveAsSnapshot: true });
887
+ }), { saveAsSnapshot: true, restoreIfDeleted: true });
888
888
  expect(mockPvSignAndSave).not.toHaveBeenCalledWith(expect.objectContaining({ packageVersionHash: "untrusted-source-value" }), expect.anything());
889
889
  });
890
890
  it("auto-activates with versionFollowRange 'latest', does not activate with 'pinned'", async () => {
@@ -11,10 +11,10 @@ exports.checkAndInstallPackageRemoteVersion = checkAndInstallPackageRemoteVersio
11
11
  exports.installRemotePackageVersion = installRemotePackageVersion;
12
12
  exports.clearRemoteCheckCache = clearRemoteCheckCache;
13
13
  const file_types_1 = require("../data/files/file.types");
14
+ const package_version_resolver_1 = require("../data/package-version-resolver");
14
15
  const package_versions_1 = require("../data/package-versions");
15
16
  const packages_1 = require("../data/packages");
16
17
  const keys_1 = require("../keys");
17
- const package_version_resolver_1 = require("../data/package-version-resolver");
18
18
  const package_author_signing_1 = require("./package-author-signing");
19
19
  const package_installer_1 = require("./package-installer");
20
20
  const package_propagation_1 = require("./package-propagation");
@@ -171,7 +171,7 @@ async function installRemotePackageVersion(dataContext, result) {
171
171
  createdAt: unpacked.payload.createdAt ?? new Date().toISOString(),
172
172
  };
173
173
  const pvTable = (0, package_versions_1.PackageVersions)(dataContext);
174
- const savedPv = await pvTable.signAndSave(pv);
174
+ const savedPv = await pvTable.signAndSave(pv, { restoreIfDeleted: true });
175
175
  // TOFU: if package has no publishPublicKey, establish it now
176
176
  if (!pkg.publishPublicKey) {
177
177
  pkg.publishPublicKey = unpacked.payload.publicKey;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.19.6",
3
+ "version": "0.19.8",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"