@peers-app/peers-sdk 0.9.0 → 0.9.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.
@@ -2,13 +2,12 @@ import { z } from 'zod';
2
2
  import { ITableMetaData } from './types';
3
3
  import { Table } from './table';
4
4
  import { ITableDefinition, TableFactory, TableConstructor } from './table-definitions.type';
5
- import type { ITableDefinitionRecord } from '../table-definitions-table';
6
5
  /**
7
6
  * Compare two table definition versions and decide whether to update.
8
- * @returns 'update' to proceed, 'skip' to keep existing, 'update-warn-unversioned' to proceed with a warning,
9
- * 'skip-warn-conflict' to skip with a warning about mismatched definitions at the same version.
7
+ * @returns 'update' when incoming version is strictly newer, 'skip' otherwise.
8
+ * Logs an error if same version but different definitions (caller should bump versionNumber).
10
9
  */
11
- export declare function checkVersionedUpdate(existingVersion: number, incomingVersion: number, existingMetaData: any, incomingMetaData: any): 'update' | 'skip' | 'update-warn-unversioned' | 'skip-warn-conflict';
10
+ export declare function checkVersionedUpdate(existingVersion: number, incomingVersion: number, existingMetaData: ITableMetaData, incomingMetaData: ITableMetaData): 'update' | 'skip';
12
11
  export declare class TableContainer {
13
12
  readonly tableFactory: TableFactory;
14
13
  readonly groupId?: string | undefined;
@@ -17,6 +16,7 @@ export declare class TableContainer {
17
16
  private savingDefinitions;
18
17
  private readonly fallbackDefinitions;
19
18
  private fallbackInstances;
19
+ private fallbackSubscribed;
20
20
  constructor(tableFactory: TableFactory, groupId?: string | undefined);
21
21
  registerTableDefinition<T extends {
22
22
  [key: string]: any;
@@ -45,12 +45,22 @@ export declare class TableContainer {
45
45
  getTableByName<T extends {
46
46
  [key: string]: any;
47
47
  }>(tableName: string): Table<T>;
48
+ /**
49
+ * Lazily look up a fallback table by name from the TableDefinitions system table.
50
+ * On first call, subscribes to TableDefinitions.dataChanged so future synced
51
+ * records are auto-registered (and removed on delete).
52
+ * Returns the table instance if found, or null if no definition exists.
53
+ */
54
+ getFallbackTable(tableName: string): Promise<Table<any> | null>;
48
55
  /**
49
56
  * Register a table definition from a synced TableDefinitions record.
50
57
  * These go into the fallback tier (no custom constructor) so they never
51
58
  * block full code-defined instances from being created later.
59
+ *
60
+ * Called automatically by the getFallbackTable dataChanged subscription.
61
+ * External code should not need to call this directly.
52
62
  */
53
- registerFromTableDefinitionRecord(record: ITableDefinitionRecord): void;
63
+ private registerFromTableDefinitionRecord;
54
64
  /**
55
65
  * This ensures all tables have been new'ed up and then returns the entire list of them.
56
66
  * Full (code-defined) instances take precedence over fallback (synced-definition) instances.
@@ -9,23 +9,36 @@ const utils_1 = require("../../utils");
9
9
  const rpc_types_1 = require("../../rpc-types");
10
10
  /**
11
11
  * Compare two table definition versions and decide whether to update.
12
- * @returns 'update' to proceed, 'skip' to keep existing, 'update-warn-unversioned' to proceed with a warning,
13
- * 'skip-warn-conflict' to skip with a warning about mismatched definitions at the same version.
12
+ * @returns 'update' when incoming version is strictly newer, 'skip' otherwise.
13
+ * Logs an error if same version but different definitions (caller should bump versionNumber).
14
14
  */
15
15
  function checkVersionedUpdate(existingVersion, incomingVersion, existingMetaData, incomingMetaData) {
16
- if (existingVersion > incomingVersion) {
17
- return 'skip';
16
+ if (existingVersion < incomingVersion) {
17
+ return 'update';
18
18
  }
19
19
  if (existingVersion === incomingVersion) {
20
- if (!incomingVersion) {
21
- return 'update-warn-unversioned';
20
+ const sameDefinition = (0, utils_1.simpleObjectHash)(existingMetaData) === (0, utils_1.simpleObjectHash)(incomingMetaData);
21
+ if (!sameDefinition) {
22
+ const tableName = (0, utils_1.getFullTableName)(existingMetaData);
23
+ console.error(`Table "${tableName}" has two different definitions with the same versionNumber (${existingMetaData.versionNumber}). Skipping update. Increment versionNumber when changing table definitions.`);
22
24
  }
23
- if ((0, utils_1.simpleObjectHash)(existingMetaData) !== (0, utils_1.simpleObjectHash)(incomingMetaData)) {
24
- return 'skip-warn-conflict';
25
- }
26
- return 'skip'; // same version, same definition
27
25
  }
28
- return 'update'; // incoming is strictly newer
26
+ return 'skip';
27
+ // commenting this out for now, it's too likely to end up in an infinite loop
28
+ // if (existingVersion > incomingVersion) {
29
+ // return 'skip';
30
+ // }
31
+ // if (existingVersion === incomingVersion) {
32
+ // const sameDefinition = simpleObjectHash(existingMetaData) === simpleObjectHash(incomingMetaData);
33
+ // if (sameDefinition) {
34
+ // return 'skip'; // same version, same definition — nothing to do
35
+ // }
36
+ // if (!incomingVersion) {
37
+ // return 'update-warn-unversioned';
38
+ // }
39
+ // return 'skip-warn-conflict';
40
+ // }
41
+ // return 'update'; // incoming is strictly newer
29
42
  }
30
43
  class TableContainer {
31
44
  tableFactory;
@@ -37,6 +50,7 @@ class TableContainer {
37
50
  // These live in a separate map so they never block full instances from being created.
38
51
  fallbackDefinitions = {};
39
52
  fallbackInstances = {};
53
+ fallbackSubscribed = false;
40
54
  constructor(tableFactory, groupId) {
41
55
  this.tableFactory = tableFactory;
42
56
  this.groupId = groupId;
@@ -53,6 +67,10 @@ class TableContainer {
53
67
  return;
54
68
  }
55
69
  if (existing && opts?.overwrite) {
70
+ const existingDef = this.tableDefinitions[tableName];
71
+ if (existingDef && (0, utils_1.simpleObjectHash)(existingDef.metaData) === (0, utils_1.simpleObjectHash)(tableDefinition.metaData)) {
72
+ return; // definition hasn't changed, no need to overwrite
73
+ }
56
74
  console.warn(`Overwriting table definition for ${tableName}.`);
57
75
  delete this.tableInstances[tableName];
58
76
  }
@@ -124,14 +142,7 @@ class TableContainer {
124
142
  tableDefsTable.get(metaData.tableId).then(existing => {
125
143
  if (existing) {
126
144
  const result = checkVersionedUpdate(existing.versionNumber, record.versionNumber, existing.metaData, record.metaData);
127
- if (result === 'update-warn-unversioned') {
128
- console.error(`Table "${tableName}" has no versionNumber. Saving anyway but this could lose data. Set metaData.versionNumber to fix this.`);
129
- }
130
- else if (result === 'skip-warn-conflict') {
131
- console.error(`Table "${tableName}" has two different definitions with the same versionNumber (${record.versionNumber}). Skipping update. Increment versionNumber when changing table definitions.`);
132
- return;
133
- }
134
- else if (result === 'skip') {
145
+ if (result === 'skip') {
135
146
  return;
136
147
  }
137
148
  }
@@ -172,10 +183,56 @@ class TableContainer {
172
183
  }
173
184
  throw new Error(`There is no instantiated table or registered definition for ${tableName}`);
174
185
  }
186
+ /**
187
+ * Lazily look up a fallback table by name from the TableDefinitions system table.
188
+ * On first call, subscribes to TableDefinitions.dataChanged so future synced
189
+ * records are auto-registered (and removed on delete).
190
+ * Returns the table instance if found, or null if no definition exists.
191
+ */
192
+ async getFallbackTable(tableName) {
193
+ // Already have an instance or definition from a previous lookup
194
+ if (this.fallbackInstances[tableName])
195
+ return this.fallbackInstances[tableName];
196
+ if (this.fallbackDefinitions[tableName]) {
197
+ const { metaData, schema } = this.fallbackDefinitions[tableName];
198
+ const table = this.tableFactory(metaData, schema ?? (0, types_1.fieldsToSchema)(metaData.fields), table_1.Table, this.groupId);
199
+ this.fallbackInstances[tableName] = table;
200
+ return table;
201
+ }
202
+ const tableDefsTable = this.tableInstances['TableDefinitions'];
203
+ if (!tableDefsTable)
204
+ return null;
205
+ // Subscribe once so future changes are picked up automatically
206
+ if (!this.fallbackSubscribed) {
207
+ this.fallbackSubscribed = true;
208
+ tableDefsTable.dataChanged.subscribe((event) => {
209
+ if (event.op === 'delete') {
210
+ const md = event.dataObject?.metaData;
211
+ if (md) {
212
+ const name = (0, utils_1.getFullTableName)(md);
213
+ delete this.fallbackDefinitions[name];
214
+ delete this.fallbackInstances[name];
215
+ }
216
+ return;
217
+ }
218
+ this.registerFromTableDefinitionRecord(event.dataObject);
219
+ });
220
+ }
221
+ // Query for this specific table definition
222
+ const record = await tableDefsTable.findOne({ name: tableName });
223
+ if (!record)
224
+ return null;
225
+ this.registerFromTableDefinitionRecord(record);
226
+ // Now instantiate via getTableByName which checks fallbackDefinitions
227
+ return this.getTableByName(tableName);
228
+ }
175
229
  /**
176
230
  * Register a table definition from a synced TableDefinitions record.
177
231
  * These go into the fallback tier (no custom constructor) so they never
178
232
  * block full code-defined instances from being created later.
233
+ *
234
+ * Called automatically by the getFallbackTable dataChanged subscription.
235
+ * External code should not need to call this directly.
179
236
  */
180
237
  registerFromTableDefinitionRecord(record) {
181
238
  const metaData = record.metaData;
@@ -194,14 +251,7 @@ class TableContainer {
194
251
  const existingVersion = existing.metaData.versionNumber ?? 0;
195
252
  const incomingVersion = record.versionNumber ?? 0;
196
253
  const result = checkVersionedUpdate(existingVersion, incomingVersion, existing.metaData, metaData);
197
- if (result === 'update-warn-unversioned') {
198
- console.error(`Table "${tableName}" has no versionNumber. Saving anyway but this could lose data. Set metaData.versionNumber to fix this.`);
199
- }
200
- else if (result === 'skip-warn-conflict') {
201
- console.error(`Table "${tableName}" has two different definitions with the same versionNumber (${incomingVersion}). Skipping update. Increment versionNumber when changing table definitions.`);
202
- return;
203
- }
204
- else if (result === 'skip') {
254
+ if (result === 'skip') {
205
255
  return;
206
256
  }
207
257
  // Remove old fallback instance so it gets re-created with the new schema
@@ -23,14 +23,17 @@ describe('checkVersionedUpdate', () => {
23
23
  it('should skip when same non-zero version and same definition with different key order', () => {
24
24
  expect((0, table_container_1.checkVersionedUpdate)(100, 100, metaA, metaAReordered)).toBe('skip');
25
25
  });
26
- it('should warn conflict when same non-zero version but different definitions', () => {
27
- expect((0, table_container_1.checkVersionedUpdate)(100, 100, metaA, metaB)).toBe('skip-warn-conflict');
26
+ it('should skip when same non-zero version but different definitions', () => {
27
+ expect((0, table_container_1.checkVersionedUpdate)(100, 100, metaA, metaB)).toBe('skip');
28
28
  });
29
- it('should warn unversioned when both versions are 0', () => {
30
- expect((0, table_container_1.checkVersionedUpdate)(0, 0, metaA, metaA)).toBe('update-warn-unversioned');
29
+ it('should skip when both versions are 0 and definitions are the same', () => {
30
+ expect((0, table_container_1.checkVersionedUpdate)(0, 0, metaA, metaA)).toBe('skip');
31
31
  });
32
- it('should warn unversioned when both versions are 0 even with different definitions', () => {
33
- expect((0, table_container_1.checkVersionedUpdate)(0, 0, metaA, metaB)).toBe('update-warn-unversioned');
32
+ it('should skip when both versions are 0 and definitions are the same with different key order', () => {
33
+ expect((0, table_container_1.checkVersionedUpdate)(0, 0, metaA, metaAReordered)).toBe('skip');
34
+ });
35
+ it('should skip when both versions are 0 and definitions differ', () => {
36
+ expect((0, table_container_1.checkVersionedUpdate)(0, 0, metaA, metaB)).toBe('skip');
34
37
  });
35
38
  it('should update when incoming is newer regardless of definition content', () => {
36
39
  expect((0, table_container_1.checkVersionedUpdate)(50, 100, metaA, metaA)).toBe('update');
@@ -86,6 +89,7 @@ describe('Tiered table instances', () => {
86
89
  it('bare-bones fallback instance should NOT block full instance with custom constructor', () => {
87
90
  const { container, testMetaData, testSchema, defRecord } = createTestHarness();
88
91
  // Simulate sync: register from a TableDefinitionRecord (no custom constructor)
92
+ // (method is private — normally auto-called via dataChanged subscription)
89
93
  container.registerFromTableDefinitionRecord(defRecord);
90
94
  // Sync code calls getTableByName to get a working instance
91
95
  const fallbackTable = container.getTableByName(defRecord.name);
@@ -99,7 +103,7 @@ describe('Tiered table instances', () => {
99
103
  });
100
104
  it('getTableByName should return full instance when one exists', () => {
101
105
  const { container, testMetaData, testSchema, defRecord } = createTestHarness();
102
- // Register fallback definition
106
+ // Register fallback definition (private — normally auto-called)
103
107
  container.registerFromTableDefinitionRecord(defRecord);
104
108
  // Package code creates the full instance first
105
109
  const fullTable = container.getTable(testMetaData, testSchema, CustomTestTable);
@@ -111,7 +115,7 @@ describe('Tiered table instances', () => {
111
115
  });
112
116
  it('getTableByName should return fallback instance when no full instance exists', () => {
113
117
  const { container, defRecord } = createTestHarness();
114
- // Only register from definition record -- no full definition available
118
+ // Only register from definition record -- no full definition available (private — normally auto-called)
115
119
  container.registerFromTableDefinitionRecord(defRecord);
116
120
  // Should return a bare-bones Table instance
117
121
  const table = container.getTableByName(defRecord.name);
@@ -138,7 +142,7 @@ describe('Tiered table instances', () => {
138
142
  metaData: metaData2,
139
143
  versionNumber: 0,
140
144
  };
141
- // Register both as fallback definitions
145
+ // Register both as fallback definitions (private — normally auto-called)
142
146
  container.registerFromTableDefinitionRecord(defRecord);
143
147
  container.registerFromTableDefinitionRecord(defRecord2);
144
148
  // Create full instance for only the first table
@@ -45,7 +45,7 @@ class Table {
45
45
  // Find any underlying SubscribableDataSource to intercept its events
46
46
  let subscribableDataSource = undefined;
47
47
  let _dataSource = this.dataSource;
48
- while (!subscribableDataSource && _dataSource) {
48
+ while (_dataSource) {
49
49
  if (!subscribableDataSource && _dataSource instanceof subscribable_data_source_1.SubscribableDataSource) {
50
50
  subscribableDataSource = _dataSource;
51
51
  }
package/dist/keys.js CHANGED
@@ -63,6 +63,9 @@ function encodeBase64(data) {
63
63
  return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
64
64
  }
65
65
  function decodeBase64(data) {
66
+ if (typeof data !== 'string') {
67
+ throw new TypeError(`decodeBase64 expected a string but received ${typeof data}`);
68
+ }
66
69
  data = data.replace(/-/g, '+').replace(/_/g, '/');
67
70
  while (data.length % 4 !== 0) {
68
71
  data += '=';
@@ -211,9 +214,12 @@ function addSignatureToObject(obj, secretKey) {
211
214
  * Will throw if the signature is invalid
212
215
  */
213
216
  function verifyObjectSignature(objectWithSignature) {
217
+ if (!objectWithSignature.signature) {
218
+ throw new Error('Object has no signature to verify');
219
+ }
214
220
  const objWithoutSignature = { ...objectWithSignature };
215
221
  delete objWithoutSignature.signature;
216
- const [objPublicKey, objSignature] = objectWithSignature.signature?.split(':') || [];
222
+ const [objPublicKey, objSignature] = objectWithSignature.signature.split(':');
217
223
  const signedObject = {
218
224
  contents: objWithoutSignature,
219
225
  signature: objSignature,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"