@peers-app/peers-sdk 0.16.3 → 0.16.5

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.
@@ -238,6 +238,7 @@ class UserContext {
238
238
  subscribeToDataChangedAcrossAllGroups(table, handler) {
239
239
  const tableName = typeof table === "string" ? table : table.tableName;
240
240
  const tableEventPrefix = `${tableName}_DataChanged_`;
241
+ const unsubscribePrefix = (0, events_1.subscribePrefix)(tableEventPrefix);
241
242
  const subscription = (0, events_1.subscribe)((evt) => evt.name.startsWith(tableEventPrefix), async (evt) => {
242
243
  const dataContextId = evt.name.endsWith("_") ? this.userId : evt.name.split("_").pop();
243
244
  const dataContext = this.getDataContext(dataContextId);
@@ -248,6 +249,11 @@ class UserContext {
248
249
  data: evt.data,
249
250
  });
250
251
  });
252
+ const originalUnsubscribe = subscription.unsubscribe;
253
+ subscription.unsubscribe = () => {
254
+ unsubscribePrefix();
255
+ return originalUnsubscribe();
256
+ };
251
257
  return subscription;
252
258
  }
253
259
  dispose() {
@@ -84,7 +84,7 @@ const getPrimaryAssistant = async (dataContext) => {
84
84
  if (a)
85
85
  return a;
86
86
  }
87
- const fallback = await Assistants(dataContext).get(system_ids_1.openCodeAssistantId);
87
+ const fallback = await Assistants(dataContext).get(system_ids_1.defaultAssistantId);
88
88
  if (!fallback)
89
89
  throw new Error("Primary assistant not found");
90
90
  return fallback;
@@ -29,6 +29,6 @@ function Devices(dataContext) {
29
29
  return (0, user_context_singleton_1.getTableContainer)(dataContext).getTable(metaData, exports.deviceSchema);
30
30
  }
31
31
  exports.trustedServers = (0, persistent_vars_1.groupVar)("trustedServers", {
32
- defaultValue: ["https://peers.app", "https://peers-services.azurewebsites.net"],
32
+ defaultValue: ["https://peers.app"],
33
33
  });
34
34
  exports.thisDeviceId = (0, persistent_vars_1.deviceVar)("thisDeviceId");
@@ -58,10 +58,12 @@ class PersistentVarsTable extends table_1.Table {
58
58
  }
59
59
  async save(persistentVar, opts) {
60
60
  if (!persistentVar.persistentVarId) {
61
- persistentVar.persistentVarId = (0, utils_1.newid)();
61
+ persistentVar.persistentVarId = (0, utils_1.deterministicPvarId)(persistentVar.name);
62
62
  }
63
63
  const dbVar = await this.get(persistentVar.persistentVarId);
64
- if (persistentVar.isSecret && persistentVar.value.value !== undefined && !rpc_types_1.isClient) {
64
+ if (persistentVar.isSecret &&
65
+ persistentVar.value.value !== undefined &&
66
+ (!rpc_types_1.isClient || (0, rpc_types_1.isSingleProcessClient)())) {
65
67
  if (!dbVar ||
66
68
  dbVar.value.value !== persistentVar.value.value ||
67
69
  dbVar.isSecret !== persistentVar.isSecret) {
@@ -204,6 +206,12 @@ function persistentVarFactory(name, opts) {
204
206
  dbRec = undefined;
205
207
  }
206
208
  }
209
+ if (dbRec && !(0, utils_1.isDeterministicPvarId)(dbRec.persistentVarId)) {
210
+ const newId = (0, utils_1.deterministicPvarId)(name);
211
+ await table.delete(dbRec);
212
+ dbRec.persistentVarId = newId;
213
+ dbRec = await table.save(dbRec);
214
+ }
207
215
  if (!dbRec) {
208
216
  dbRec = {
209
217
  persistentVarId: "",
@@ -271,6 +279,10 @@ function persistentVarFactory(name, opts) {
271
279
  };
272
280
  // subscribe to db changes
273
281
  userContext.subscribeToDataChangedAcrossAllGroups(exports.persistentVarsMetaData.name, async (evt) => {
282
+ const dc = getDataContext();
283
+ if (dc && evt.dataContext && evt.dataContext.dataContextId !== dc.dataContextId) {
284
+ return;
285
+ }
274
286
  const dbRec = evt.data.dataObject;
275
287
  const dbName = getVarNameInDb();
276
288
  if (!rec?.persistentVarId && dbRec.name === dbName) {
package/dist/events.d.ts CHANGED
@@ -4,10 +4,23 @@ export type IEventHandler<T = any> = (eventData: IEventData<T>) => boolean | voi
4
4
  export interface ISubscription {
5
5
  filter: IEventFilter;
6
6
  handler: IEventHandler;
7
+ /** When set, this subscription matches only events with this exact name (fast-path via Map lookup). */
8
+ eventName?: string;
7
9
  }
8
10
  export interface ISubscriptionResult {
9
11
  unsubscribe: () => boolean;
10
12
  }
13
+ /**
14
+ * Notify the events system that the client connection is ready.
15
+ * Flushes any subscriptions that were registered before the connection.
16
+ */
17
+ export declare function notifyClientConnected(): void;
18
+ /**
19
+ * Register a prefix-based event subscription with the server.
20
+ * Events whose name starts with this prefix will be forwarded to the client.
21
+ * Returns an unsubscribe function that decrements the refcount.
22
+ */
23
+ export declare function subscribePrefix(prefix: string): () => void;
11
24
  export declare function subscribe<T = any>(nameOrFilter: string | IEventFilter, handler: IEventHandler<T>): ISubscriptionResult;
12
25
  export declare function subscribeDebounce<T = any>(nameOrFilter: string | IEventFilter, handler: IEventHandler<T>, debounceMs: number): ISubscriptionResult;
13
26
  export declare function emit(event: IEventData, dontPropagate?: boolean): Promise<boolean>;
package/dist/events.js CHANGED
@@ -1,37 +1,195 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Emitter = exports.Event = void 0;
4
+ exports.notifyClientConnected = notifyClientConnected;
5
+ exports.subscribePrefix = subscribePrefix;
4
6
  exports.subscribe = subscribe;
5
7
  exports.subscribeDebounce = subscribeDebounce;
6
8
  exports.emit = emit;
7
9
  exports.unionEvents = unionEvents;
8
10
  const rpc_types_1 = require("./rpc-types");
9
11
  const unitTests = process.env.NODE_ENV === "test";
10
- const subscriptions = [];
11
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ // ─── Subscription Storage ────────────────────────────────────────────────────
13
+ /** Subscriptions indexed by exact event name for O(1) dispatch. */
14
+ const nameIndex = new Map();
15
+ /** Subscriptions that use custom filter functions (checked on every emit). */
16
+ const wildcardSubscriptions = new Set();
17
+ function addSubscription(subscription) {
18
+ if (subscription.eventName) {
19
+ let bucket = nameIndex.get(subscription.eventName);
20
+ if (!bucket) {
21
+ bucket = new Set();
22
+ nameIndex.set(subscription.eventName, bucket);
23
+ }
24
+ bucket.add(subscription);
25
+ }
26
+ else {
27
+ wildcardSubscriptions.add(subscription);
28
+ }
29
+ }
30
+ function removeSubscription(subscription) {
31
+ if (subscription.eventName) {
32
+ const bucket = nameIndex.get(subscription.eventName);
33
+ if (bucket) {
34
+ const removed = bucket.delete(subscription);
35
+ if (bucket.size === 0) {
36
+ nameIndex.delete(subscription.eventName);
37
+ }
38
+ return removed;
39
+ }
40
+ return false;
41
+ }
42
+ return wildcardSubscriptions.delete(subscription);
43
+ }
44
+ // ─── Client → Server Subscription Registration ──────────────────────────────
45
+ // In multi-process mode (Electron), the client tells the server which event
46
+ // names/prefixes it cares about so the server can skip forwarding unneeded events.
47
+ /** Reference counts for event names registered with the server. */
48
+ const clientNameRefCounts = new Map();
49
+ /** Reference counts for event prefixes registered with the server. */
50
+ const clientPrefixRefCounts = new Map();
51
+ /** Whether the client-server connection is established (for deferred registration). */
52
+ let clientConnected = false;
53
+ /** Names pending registration (accumulated before connection is ready). */
54
+ const pendingNameSubscriptions = new Set();
55
+ /** Prefixes pending registration (accumulated before connection is ready). */
56
+ const pendingPrefixSubscriptions = new Set();
57
+ /**
58
+ * Notify the events system that the client connection is ready.
59
+ * Flushes any subscriptions that were registered before the connection.
60
+ */
61
+ function notifyClientConnected() {
62
+ clientConnected = true;
63
+ if (pendingNameSubscriptions.size > 0) {
64
+ const names = [...pendingNameSubscriptions];
65
+ pendingNameSubscriptions.clear();
66
+ rpc_types_1.rpcServerCalls.subscribeEvents(names).catch((err) => {
67
+ console.error("Failed to register pending event subscriptions with server", err);
68
+ });
69
+ }
70
+ if (pendingPrefixSubscriptions.size > 0) {
71
+ const prefixes = [...pendingPrefixSubscriptions];
72
+ pendingPrefixSubscriptions.clear();
73
+ rpc_types_1.rpcServerCalls.subscribePrefixes(prefixes).catch((err) => {
74
+ console.error("Failed to register pending prefix subscriptions with server", err);
75
+ });
76
+ }
77
+ }
78
+ function clientRegisterName(name) {
79
+ if (unitTests || (0, rpc_types_1.isSingleProcessClient)())
80
+ return;
81
+ const count = clientNameRefCounts.get(name) || 0;
82
+ clientNameRefCounts.set(name, count + 1);
83
+ if (count === 0) {
84
+ if (clientConnected) {
85
+ rpc_types_1.rpcServerCalls.subscribeEvents([name]).catch((err) => {
86
+ console.error(`Failed to register event subscription with server: ${name}`, err);
87
+ });
88
+ }
89
+ else {
90
+ pendingNameSubscriptions.add(name);
91
+ }
92
+ }
93
+ }
94
+ function clientUnregisterName(name) {
95
+ if (unitTests || (0, rpc_types_1.isSingleProcessClient)())
96
+ return;
97
+ const count = clientNameRefCounts.get(name) || 0;
98
+ if (count <= 1) {
99
+ clientNameRefCounts.delete(name);
100
+ pendingNameSubscriptions.delete(name);
101
+ if (clientConnected) {
102
+ rpc_types_1.rpcServerCalls.unsubscribeEvents([name]).catch((err) => {
103
+ console.error(`Failed to unregister event subscription with server: ${name}`, err);
104
+ });
105
+ }
106
+ }
107
+ else {
108
+ clientNameRefCounts.set(name, count - 1);
109
+ }
110
+ }
111
+ /**
112
+ * Register a prefix-based event subscription with the server.
113
+ * Events whose name starts with this prefix will be forwarded to the client.
114
+ * Returns an unsubscribe function that decrements the refcount.
115
+ */
116
+ function subscribePrefix(prefix) {
117
+ if (!rpc_types_1.isClient || unitTests || (0, rpc_types_1.isSingleProcessClient)()) {
118
+ return () => { };
119
+ }
120
+ const count = clientPrefixRefCounts.get(prefix) || 0;
121
+ clientPrefixRefCounts.set(prefix, count + 1);
122
+ if (count === 0) {
123
+ if (clientConnected) {
124
+ rpc_types_1.rpcServerCalls.subscribePrefixes([prefix]).catch((err) => {
125
+ console.error(`Failed to register prefix subscription with server: ${prefix}`, err);
126
+ });
127
+ }
128
+ else {
129
+ pendingPrefixSubscriptions.add(prefix);
130
+ }
131
+ }
132
+ return () => {
133
+ const current = clientPrefixRefCounts.get(prefix) || 0;
134
+ if (current <= 1) {
135
+ clientPrefixRefCounts.delete(prefix);
136
+ pendingPrefixSubscriptions.delete(prefix);
137
+ if (clientConnected) {
138
+ rpc_types_1.rpcServerCalls.unsubscribePrefixes([prefix]).catch((err) => {
139
+ console.error(`Failed to unregister prefix subscription with server: ${prefix}`, err);
140
+ });
141
+ }
142
+ }
143
+ else {
144
+ clientPrefixRefCounts.set(prefix, current - 1);
145
+ }
146
+ };
147
+ }
148
+ // ─── Server-Side Event Forwarding Gate ───────────────────────────────────────
149
+ // The server uses this to decide whether to forward an event to the client.
150
+ /** Event names the client has subscribed to. Server-side only. */
151
+ const serverClientNames = new Set();
152
+ /** Event prefixes the client has subscribed to. Server-side only. */
153
+ const serverClientPrefixes = new Set();
154
+ /**
155
+ * Check whether the client wants this event.
156
+ * Used by the server to gate `rpcClientCalls.emitEvent`.
157
+ */
158
+ function clientWantsEvent(event) {
159
+ if (serverClientNames.has(event.name))
160
+ return true;
161
+ for (const prefix of serverClientPrefixes) {
162
+ if (event.name.startsWith(prefix))
163
+ return true;
164
+ }
165
+ return false;
166
+ }
167
+ // ─── Public API ──────────────────────────────────────────────────────────────
12
168
  function subscribe(nameOrFilter, handler) {
169
+ let eventName;
170
+ let filter;
13
171
  if (typeof nameOrFilter === "string") {
14
- const name = nameOrFilter;
15
- nameOrFilter = (evt) => evt.name === name;
172
+ eventName = nameOrFilter;
173
+ filter = (evt) => evt.name === eventName;
174
+ }
175
+ else {
176
+ filter = nameOrFilter;
177
+ }
178
+ const subscription = { filter, handler, eventName };
179
+ addSubscription(subscription);
180
+ if (rpc_types_1.isClient && eventName) {
181
+ clientRegisterName(eventName);
16
182
  }
17
- const filter = nameOrFilter;
18
- const subscription = {
19
- filter,
20
- handler,
21
- };
22
- subscriptions.push(subscription);
23
183
  return {
24
184
  unsubscribe: () => {
25
- const iSub = subscriptions.indexOf(subscription);
26
- if (iSub >= 0) {
27
- subscriptions.splice(iSub, 1);
28
- return true;
185
+ const removed = removeSubscription(subscription);
186
+ if (removed && rpc_types_1.isClient && eventName) {
187
+ clientUnregisterName(eventName);
29
188
  }
30
- return false;
189
+ return removed;
31
190
  },
32
191
  };
33
192
  }
34
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
193
  function subscribeDebounce(nameOrFilter, handler, debounceMs) {
36
194
  let pid = 0;
37
195
  const handlerDebounced = (evt) => {
@@ -46,37 +204,64 @@ function subscribeDebounce(nameOrFilter, handler, debounceMs) {
46
204
  return subscribe(nameOrFilter, handlerDebounced);
47
205
  }
48
206
  async function emit(event, dontPropagate) {
49
- const matchedHandlerPromises = subscriptions
50
- .filter((subscription) => subscription.filter(event))
51
- .map(async (subscription) => {
207
+ const matched = [];
208
+ const bucket = nameIndex.get(event.name);
209
+ if (bucket) {
210
+ for (const sub of bucket) {
211
+ matched.push(sub);
212
+ }
213
+ }
214
+ for (const sub of wildcardSubscriptions) {
215
+ if (sub.filter(event)) {
216
+ matched.push(sub);
217
+ }
218
+ }
219
+ const handlerPromises = matched.map(async (subscription) => {
52
220
  try {
53
221
  return await subscription.handler(event);
54
222
  }
55
223
  catch (err) {
56
- console.error(`An unhandled error occurred in a handler while processing event: ${JSON.stringify({ event, subscription })}`, err);
224
+ console.error(`An unhandled error occurred in a handler while processing event: ${JSON.stringify({ event })}`, err);
57
225
  return false;
58
226
  }
59
227
  });
60
- // if (!(dontPropagate || unitTests)) {
61
- // matchedHandlerPromises.push(
62
- // isClient
63
- // ? rpcServerCalls.emitEvent(event).catch((err) => { console.error(`Error while propagating event to server`, err); return false; })
64
- // : rpcClientCalls.emitEvent(event).catch((err) => { console.error(`Error while propagating event to client`, err); return false; })
65
- // );
66
- // }
67
- if (!(dontPropagate || unitTests)) {
68
- matchedHandlerPromises.push(rpc_types_1.rpcClientCalls.emitEvent(event).catch((err) => {
69
- console.error(`Error while propagating event to client`, err);
70
- return false;
71
- }));
228
+ if (!(dontPropagate || unitTests || (0, rpc_types_1.isSingleProcessClient)())) {
229
+ if (clientWantsEvent(event)) {
230
+ handlerPromises.push(rpc_types_1.rpcClientCalls.emitEvent(event).catch((err) => {
231
+ console.error(`Error while propagating event to client`, err);
232
+ return false;
233
+ }));
234
+ }
72
235
  }
73
- const results = await Promise.all(matchedHandlerPromises);
74
- // if any handlers returned false (or errored), return false, otherwise return true
236
+ const results = await Promise.all(handlerPromises);
75
237
  return !results.some((r) => r === false);
76
238
  }
77
239
  if (rpc_types_1.isClient) {
78
240
  rpc_types_1.rpcClientCalls.emitEvent = (event) => emit(event, true);
79
241
  }
242
+ // Server-side: register RPC handlers for client subscription management
243
+ if (!rpc_types_1.isClient) {
244
+ rpc_types_1.rpcServerCalls.subscribeEvents = async (names) => {
245
+ for (const name of names) {
246
+ serverClientNames.add(name);
247
+ }
248
+ };
249
+ rpc_types_1.rpcServerCalls.unsubscribeEvents = async (names) => {
250
+ for (const name of names) {
251
+ serverClientNames.delete(name);
252
+ }
253
+ };
254
+ rpc_types_1.rpcServerCalls.subscribePrefixes = async (prefixes) => {
255
+ for (const prefix of prefixes) {
256
+ serverClientPrefixes.add(prefix);
257
+ }
258
+ };
259
+ rpc_types_1.rpcServerCalls.unsubscribePrefixes = async (prefixes) => {
260
+ for (const prefix of prefixes) {
261
+ serverClientPrefixes.delete(prefix);
262
+ }
263
+ };
264
+ }
80
265
  class Event {
81
266
  _eventName;
82
267
  constructor(_eventName) {
@@ -119,7 +304,6 @@ class Emitter {
119
304
  }
120
305
  }
121
306
  exports.Emitter = Emitter;
122
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
123
307
  function unionEvents(...events) {
124
308
  const eventName = events.map((s) => s.eventName()).join("|");
125
309
  return {
package/dist/keys.js CHANGED
@@ -221,15 +221,25 @@ function encryptString(data, secretKey) {
221
221
  return `${encodeBase64(encryptedData)}~${encodeBase64(nonce)}`;
222
222
  }
223
223
  function decryptString(data, secretKey) {
224
- const _secretKey = decodeBase64(secretKey);
225
- const [encryptedData, nonce] = data.split("~");
226
- const _data = decodeBase64(encryptedData);
227
- const _nonce = decodeBase64(nonce);
228
- const decryptedData = nacl.secretbox.open(_data, _nonce, _secretKey.slice(0, 32));
229
- if (decryptedData === null) {
230
- throw new Error("Failed to decrypt data");
224
+ const preview = typeof data === "string" ? data.slice(0, 24) : String(data);
225
+ const hasSeparator = typeof data === "string" && data.includes("~");
226
+ try {
227
+ const _secretKey = decodeBase64(secretKey);
228
+ const [encryptedData, nonce] = data.split("~");
229
+ const _data = decodeBase64(encryptedData);
230
+ const _nonce = decodeBase64(nonce);
231
+ const decryptedData = nacl.secretbox.open(_data, _nonce, _secretKey.slice(0, 32));
232
+ if (decryptedData === null) {
233
+ throw new Error("secretbox.open returned null (wrong key or corrupted ciphertext)");
234
+ }
235
+ return (0, tweetnacl_util_1.encodeUTF8)(decryptedData);
236
+ }
237
+ catch (err) {
238
+ const reason = hasSeparator
239
+ ? "base64 decode or secretbox failed"
240
+ : "value does not look encrypted (missing '~' separator — was it saved without encryption?)";
241
+ throw new Error(`decryptString failed: ${reason}. Preview: "${preview}…" | Original error: ${err?.message ?? err}`);
231
242
  }
232
- return (0, tweetnacl_util_1.encodeUTF8)(decryptedData);
233
243
  }
234
244
  /** JSON-serializes `data` then NaCl `secretbox` with a random nonce; returns base64 payload `~` base64 nonce. */
235
245
  function encryptData(data, secretKey) {
@@ -116,6 +116,10 @@ export declare const rpcServerCalls: {
116
116
  voiceDisable: () => Promise<void>;
117
117
  voiceEnable: () => Promise<void>;
118
118
  flushDatabases: () => Promise<void>;
119
+ subscribeEvents: (names: string[]) => Promise<void>;
120
+ unsubscribeEvents: (names: string[]) => Promise<void>;
121
+ subscribePrefixes: (prefixes: string[]) => Promise<void>;
122
+ unsubscribePrefixes: (prefixes: string[]) => Promise<void>;
119
123
  };
120
124
  export declare const rpcClientCalls: {
121
125
  ping: (msg: string) => Promise<string>;
package/dist/rpc-types.js CHANGED
@@ -67,10 +67,11 @@ exports.rpcServerCalls = {
67
67
  // Flush all in-memory database snapshots to durable storage (IndexedDB in PWA).
68
68
  // No-op on Electron where better-sqlite3 writes are synchronous to disk.
69
69
  flushDatabases: (async () => { }),
70
- // TODO try to get rid of this and rely on the client-side table and server-side table individually emitting events
71
- // TODO TODO before deleting this, check if we can stop client-side tables from emitting events and rely solely on server-side tables
72
- // propagating events with rpcClientCalls.emitEvent. It's very likely we're currently seeing two events for every one write originating from the UI
73
- // emitEvent: _na as ((event: IEventData) => Promise<boolean>),
70
+ // Event subscription management (for selective proxying)
71
+ subscribeEvents: rpcStub("subscribeEvents"),
72
+ unsubscribeEvents: rpcStub("unsubscribeEvents"),
73
+ subscribePrefixes: rpcStub("subscribePrefixes"),
74
+ unsubscribePrefixes: rpcStub("unsubscribePrefixes"),
74
75
  };
75
76
  exports.rpcClientCalls = {
76
77
  ping: async (msg) => `pong: ${msg}`,
package/dist/utils.d.ts CHANGED
@@ -31,6 +31,21 @@ export declare function simpleHash(str: string): number;
31
31
  export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
32
32
  /** `simpleHash` of JSON-stable stringification of an object. */
33
33
  export declare function simpleObjectHash(obj: any): number;
34
+ /**
35
+ * Generates a deterministic 25-character peer ID for a persistent variable.
36
+ * The ID has the fixed prefix `000pvar0` (8 chars) followed by 17 base-36 characters
37
+ * derived from a SHA-512 hash of the input name. Two calls with the same name always
38
+ * return the same ID.
39
+ *
40
+ * @param name The persistent variable name as stored in the database
41
+ * (already includes group suffixes for groupDevice/groupUser scopes)
42
+ */
43
+ export declare function deterministicPvarId(name: string): string;
44
+ /**
45
+ * Returns whether a peer ID was generated by {@link deterministicPvarId}
46
+ * (starts with the `000pvar0` prefix).
47
+ */
48
+ export declare function isDeterministicPvarId(id: string): boolean;
34
49
  /** Resolves after `ms` milliseconds (0 yields a microtask-yield on most runtimes). */
35
50
  export declare function sleep(ms?: number): Promise<void>;
36
51
  /** Turns `fooBar` into `Foo Bar` for labels (also replaces `_` with space). */
package/dist/utils.js CHANGED
@@ -9,6 +9,8 @@ exports.idDate = idDate;
9
9
  exports.idRandNums = idRandNums;
10
10
  exports.simpleHash = simpleHash;
11
11
  exports.simpleObjectHash = simpleObjectHash;
12
+ exports.deterministicPvarId = deterministicPvarId;
13
+ exports.isDeterministicPvarId = isDeterministicPvarId;
12
14
  exports.sleep = sleep;
13
15
  exports.camelCaseToSpaces = camelCaseToSpaces;
14
16
  exports.camelCaseToHyphens = camelCaseToHyphens;
@@ -119,6 +121,43 @@ function simpleHash(str) {
119
121
  function simpleObjectHash(obj) {
120
122
  return simpleHash(stableStringify(obj));
121
123
  }
124
+ /** Well-known prefix for deterministic persistent variable IDs. */
125
+ const DETERMINISTIC_PVAR_PREFIX = "000pvar0";
126
+ /**
127
+ * Converts a byte array to a base-36 string of the requested length, using unbiased modular reduction.
128
+ * Bytes >= 252 are skipped to avoid bias (same technique as {@link cryptoRandomString}).
129
+ */
130
+ function bytesToBase36(bytes, length) {
131
+ let s = "";
132
+ for (let i = 0; i < bytes.length && s.length < length; i++) {
133
+ const n = bytes[i];
134
+ if (n >= 252)
135
+ continue;
136
+ s += (n % 36).toString(36);
137
+ }
138
+ return s.substring(0, length);
139
+ }
140
+ /**
141
+ * Generates a deterministic 25-character peer ID for a persistent variable.
142
+ * The ID has the fixed prefix `000pvar0` (8 chars) followed by 17 base-36 characters
143
+ * derived from a SHA-512 hash of the input name. Two calls with the same name always
144
+ * return the same ID.
145
+ *
146
+ * @param name The persistent variable name as stored in the database
147
+ * (already includes group suffixes for groupDevice/groupUser scopes)
148
+ */
149
+ function deterministicPvarId(name) {
150
+ const hashSuffixLength = 25 - DETERMINISTIC_PVAR_PREFIX.length; // 17
151
+ const hash = nacl.hash(new TextEncoder().encode(name)); // 64-byte SHA-512
152
+ return DETERMINISTIC_PVAR_PREFIX + bytesToBase36(hash, hashSuffixLength);
153
+ }
154
+ /**
155
+ * Returns whether a peer ID was generated by {@link deterministicPvarId}
156
+ * (starts with the `000pvar0` prefix).
157
+ */
158
+ function isDeterministicPvarId(id) {
159
+ return id.startsWith(DETERMINISTIC_PVAR_PREFIX);
160
+ }
122
161
  /** Resolves after `ms` milliseconds (0 yields a microtask-yield on most runtimes). */
123
162
  function sleep(ms = 0) {
124
163
  return new Promise((resolve) => setTimeout(resolve, ms));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.16.3",
3
+ "version": "0.16.5",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"