@peers-app/peers-sdk 0.26.6 → 0.27.1

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.
Files changed (189) hide show
  1. package/README.md +3 -1
  2. package/bin/peers-package.js +13 -0
  3. package/define/package.json +4 -0
  4. package/dist/contracts/__tests__/action-dispatch.test.js +56 -76
  5. package/dist/contracts/__tests__/also-implements.test.d.ts +1 -0
  6. package/dist/contracts/__tests__/also-implements.test.js +110 -0
  7. package/dist/contracts/__tests__/builder.test.js +45 -0
  8. package/dist/contracts/__tests__/contract-resolvers.test.js +134 -4
  9. package/dist/contracts/__tests__/frozen-contracts.test.d.ts +1 -0
  10. package/dist/contracts/__tests__/frozen-contracts.test.js +191 -0
  11. package/dist/contracts/__tests__/generic-tool-dispatch.test.js +13 -56
  12. package/dist/contracts/__tests__/host-contract-client.test.d.ts +1 -0
  13. package/dist/contracts/__tests__/host-contract-client.test.js +793 -0
  14. package/dist/contracts/__tests__/host-contract-transport.test.js +15 -0
  15. package/dist/contracts/__tests__/registry.test.js +68 -0
  16. package/dist/contracts/__tests__/target-dispatch.d.ts +11 -0
  17. package/dist/contracts/__tests__/target-dispatch.js +24 -0
  18. package/dist/contracts/__tests__/tool-catalog-service.test.js +37 -1
  19. package/dist/contracts/__tests__/tool-catalog.test.js +12 -3
  20. package/dist/contracts/__tests__/validate.test.js +177 -7
  21. package/dist/contracts/__tests__/workflow-context.test.js +0 -11
  22. package/dist/contracts/builder.d.ts +16 -2
  23. package/dist/contracts/builder.js +44 -3
  24. package/dist/contracts/contract-definition-schema.d.ts +251 -0
  25. package/dist/contracts/contract-definition-schema.js +48 -0
  26. package/dist/contracts/contract-provider-router.d.ts +2 -0
  27. package/dist/contracts/contract-provider-router.js +5 -2
  28. package/dist/contracts/contract-proxy.d.ts +52 -18
  29. package/dist/contracts/contract-proxy.js +144 -203
  30. package/dist/contracts/contract-resolvers.d.ts +6 -4
  31. package/dist/contracts/contract-resolvers.js +32 -9
  32. package/dist/contracts/frozen-contracts.d.ts +59 -0
  33. package/dist/contracts/frozen-contracts.js +86 -0
  34. package/dist/contracts/host-contract-client.d.ts +134 -0
  35. package/dist/contracts/host-contract-client.js +396 -0
  36. package/dist/contracts/host-contract-transport.d.ts +2 -0
  37. package/dist/contracts/host-contract-transport.js +8 -0
  38. package/dist/contracts/index.d.ts +6 -4
  39. package/dist/contracts/index.js +11 -5
  40. package/dist/contracts/registry.d.ts +31 -1
  41. package/dist/contracts/registry.js +100 -6
  42. package/dist/contracts/system/contract-catalog-contract.d.ts +61 -6
  43. package/dist/contracts/system/contract-catalog-contract.js +17 -5
  44. package/dist/contracts/tool-catalog-service.js +26 -6
  45. package/dist/contracts/tool-catalog.d.ts +1 -1
  46. package/dist/contracts/tool-catalog.js +4 -1
  47. package/dist/contracts/types.d.ts +32 -2
  48. package/dist/contracts/validate.d.ts +16 -1
  49. package/dist/contracts/validate.js +139 -25
  50. package/dist/data/change-tracking.d.ts +17 -5
  51. package/dist/data/change-tracking.js +22 -6
  52. package/dist/data/change-tracking.test.d.ts +1 -0
  53. package/dist/data/change-tracking.test.js +62 -0
  54. package/dist/data/create-group.js +1 -1
  55. package/dist/data/group-members.d.ts +14 -0
  56. package/dist/data/group-members.js +36 -0
  57. package/dist/data/group-members.test.js +49 -0
  58. package/dist/data/orm/sql.data-source.d.ts +9 -0
  59. package/dist/data/orm/sql.data-source.js +47 -2
  60. package/dist/data/orm/sql.data-source.test.js +142 -0
  61. package/dist/data/orm/table-container.js +9 -9
  62. package/dist/data/orm/table-container.test.js +44 -0
  63. package/dist/data/orm/types.d.ts +41 -1
  64. package/dist/data/orm/types.js +159 -28
  65. package/dist/data/orm/types.test.js +129 -0
  66. package/dist/data/peer-types.d.ts +8 -62
  67. package/dist/data/persistent-vars.d.ts +4 -1
  68. package/dist/data/persistent-vars.js +7 -2
  69. package/dist/data/tools.d.ts +29 -221
  70. package/dist/define/also-implements.test.d.ts +1 -0
  71. package/dist/define/also-implements.test.js +123 -0
  72. package/dist/define/context.d.ts +77 -0
  73. package/dist/define/context.js +2 -0
  74. package/dist/define/contract-ref.d.ts +177 -0
  75. package/dist/define/contract-ref.js +67 -0
  76. package/dist/define/declare-contract.d.ts +167 -0
  77. package/dist/define/declare-contract.js +220 -0
  78. package/dist/define/declare-contract.test.d.ts +1 -0
  79. package/dist/define/declare-contract.test.js +573 -0
  80. package/dist/define/define-package.d.ts +203 -0
  81. package/dist/define/define-package.js +289 -0
  82. package/dist/define/define.test.d.ts +1 -0
  83. package/dist/define/define.test.js +1010 -0
  84. package/dist/define/empty-fields.d.ts +15 -0
  85. package/dist/define/empty-fields.js +134 -0
  86. package/dist/define/empty-fields.test.d.ts +1 -0
  87. package/dist/define/empty-fields.test.js +55 -0
  88. package/dist/define/events-observables.test.d.ts +1 -0
  89. package/dist/define/events-observables.test.js +1013 -0
  90. package/dist/define/events.d.ts +81 -0
  91. package/dist/define/events.js +81 -0
  92. package/dist/define/generated-field.d.ts +20 -0
  93. package/dist/define/generated-field.js +25 -0
  94. package/dist/define/index.d.ts +28 -0
  95. package/dist/define/index.js +62 -0
  96. package/dist/define/lazy-members.d.ts +8 -0
  97. package/dist/define/lazy-members.js +38 -0
  98. package/dist/define/manifest.d.ts +63 -0
  99. package/dist/define/manifest.js +653 -0
  100. package/dist/define/observables.d.ts +146 -0
  101. package/dist/define/observables.js +187 -0
  102. package/dist/define/serve-members.d.ts +21 -0
  103. package/dist/define/serve-members.js +152 -0
  104. package/dist/define/serve-members.test.d.ts +1 -0
  105. package/dist/define/serve-members.test.js +630 -0
  106. package/dist/define/serve.d.ts +22 -0
  107. package/dist/define/serve.js +58 -0
  108. package/dist/define/system-contracts.d.ts +41 -0
  109. package/dist/define/system-contracts.js +21 -0
  110. package/dist/define/table-handle.d.ts +13 -0
  111. package/dist/define/table-handle.js +36 -0
  112. package/dist/define/table.d.ts +128 -0
  113. package/dist/define/table.js +122 -0
  114. package/dist/define/tool.d.ts +129 -0
  115. package/dist/define/tool.js +91 -0
  116. package/dist/define/vars.d.ts +82 -0
  117. package/dist/define/vars.js +256 -0
  118. package/dist/define/vars.test.d.ts +1 -0
  119. package/dist/define/vars.test.js +39 -0
  120. package/dist/define/wire-value.d.ts +12 -0
  121. package/dist/define/wire-value.js +76 -0
  122. package/dist/define/worker-state.d.ts +15 -0
  123. package/dist/define/worker-state.js +45 -0
  124. package/dist/device/tx-encoding.test.js +10 -0
  125. package/dist/index.d.ts +3 -0
  126. package/dist/index.js +5 -1
  127. package/dist/invites/group-approval.js +4 -2
  128. package/dist/invites/group-approval.test.js +2 -0
  129. package/dist/observable.d.ts +42 -2
  130. package/dist/observable.js +25 -7
  131. package/dist/package-build/build-package.d.ts +60 -0
  132. package/dist/package-build/build-package.js +312 -0
  133. package/dist/package-build/build-package.test.d.ts +1 -0
  134. package/dist/package-build/build-package.test.js +204 -0
  135. package/dist/package-build/cli.d.ts +7 -0
  136. package/dist/package-build/cli.js +218 -0
  137. package/dist/package-build/contract-add.d.ts +35 -0
  138. package/dist/package-build/contract-add.js +97 -0
  139. package/dist/package-build/contract-add.test.d.ts +1 -0
  140. package/dist/package-build/contract-add.test.js +467 -0
  141. package/dist/package-build/contract-source.d.ts +59 -0
  142. package/dist/package-build/contract-source.js +471 -0
  143. package/dist/package-build/freeze-contracts.d.ts +56 -0
  144. package/dist/package-build/freeze-contracts.js +198 -0
  145. package/dist/package-build/freeze-contracts.test.d.ts +1 -0
  146. package/dist/package-build/freeze-contracts.test.js +355 -0
  147. package/dist/package-installer/package-creator.d.ts +4 -0
  148. package/dist/package-installer/package-creator.js +57 -16
  149. package/dist/package-installer/package-creator.test.js +86 -0
  150. package/dist/package-loader/contract-members.d.ts +53 -0
  151. package/dist/package-loader/contract-members.js +31 -0
  152. package/dist/package-loader/contract-package-loader.js +5 -1
  153. package/dist/package-loader/contract-package-loader.test.js +11 -0
  154. package/dist/package-loader/host-authority-loading.test.js +9 -5
  155. package/dist/package-loader/index.d.ts +1 -0
  156. package/dist/package-loader/index.js +1 -0
  157. package/dist/package-loader/isolated-artifact.d.ts +80 -17
  158. package/dist/package-loader/isolated-artifact.js +257 -51
  159. package/dist/package-loader/isolated-artifact.test.js +294 -21
  160. package/dist/package-loader/isolated-runtime.d.ts +22 -7
  161. package/dist/package-loader/isolated-table-gateway.js +30 -11
  162. package/dist/package-loader/isolated-table-gateway.test.js +243 -0
  163. package/dist/package-loader/package-loader.d.ts +8 -1
  164. package/dist/package-loader/package-loader.js +39 -20
  165. package/dist/package-loader/package-loader.test.js +47 -0
  166. package/dist/peer-id.d.ts +25 -0
  167. package/dist/peer-id.js +99 -0
  168. package/dist/serial-json.compat.test.d.ts +1 -0
  169. package/dist/serial-json.compat.test.js +369 -0
  170. package/dist/serial-json.d.ts +41 -5
  171. package/dist/serial-json.js +216 -157
  172. package/dist/serial-json.oracle.d.ts +21 -0
  173. package/dist/serial-json.oracle.js +222 -0
  174. package/dist/serial-json.test.js +315 -0
  175. package/dist/testing/index.d.ts +4 -0
  176. package/dist/testing/index.js +8 -0
  177. package/dist/testing/mock-host-contract.d.ts +123 -0
  178. package/dist/testing/mock-host-contract.js +434 -0
  179. package/dist/testing/mock-host-contract.test.d.ts +1 -0
  180. package/dist/testing/mock-host-contract.test.js +782 -0
  181. package/dist/types/field-type.d.ts +35 -18
  182. package/dist/types/field-type.js +4 -1
  183. package/dist/types/zod-types.d.ts +9 -0
  184. package/dist/types/zod-types.js +8 -2
  185. package/dist/utils.d.ts +22 -25
  186. package/dist/utils.js +45 -92
  187. package/dist/utils.test.js +36 -0
  188. package/package.json +9 -2
  189. package/testing/package.json +4 -0
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ /** The value types a Peers IR field can have. */
2
3
  export declare enum FieldType {
3
4
  id = "id",
4
5
  string = "string",
@@ -9,6 +10,37 @@ export declare enum FieldType {
9
10
  blob = "blob",
10
11
  any = "any"
11
12
  }
13
+ /**
14
+ * One field in Peers IR: a table column, a tool input or output, an event payload field, or
15
+ * (without `name`) an observable's value.
16
+ *
17
+ * The Runtime checks `type`, `optional`, and `isArray` on table rows, tool arguments, and event
18
+ * payloads (an observable's value, against its `valueType`). `fields` and `enumValues` describe
19
+ * the value further, for the models that call tools and for generated types; the package's own
20
+ * schema enforces them.
21
+ *
22
+ * A field has one empty value: missing. The Runtime removes an optional field that is `null` or
23
+ * `undefined` (a field with a default gets the default instead), stores it as SQL NULL, and
24
+ * reads it back unset.
25
+ */
26
+ export interface IField {
27
+ name: string;
28
+ description?: string;
29
+ type: FieldType;
30
+ defaultValue?: any;
31
+ /** The value may be empty: missing, `undefined`, or `null`, all read back as missing. */
32
+ optional?: boolean;
33
+ /** The value is an array of `type`; `fields`, `enumValues` describe each element. */
34
+ isArray?: boolean;
35
+ subType?: string;
36
+ /** An object's own fields. Absent: any object. Only on `object`; other keys may be present. */
37
+ fields?: IField[];
38
+ /** The only values allowed: strings, numbers, or booleans matching `type` (any of them on `any`). */
39
+ enumValues?: (string | number | boolean)[];
40
+ }
41
+ /** A field's shape without its name: an observable's value. */
42
+ export type IFieldShape = Omit<IField, "name">;
43
+ /** Zod schema for {@link IField}, used where rows store field lists. */
12
44
  export declare const fieldSchema: z.ZodObject<{
13
45
  name: z.ZodString;
14
46
  description: z.ZodOptional<z.ZodString>;
@@ -17,21 +49,6 @@ export declare const fieldSchema: z.ZodObject<{
17
49
  optional: z.ZodOptional<z.ZodBoolean>;
18
50
  isArray: z.ZodOptional<z.ZodBoolean>;
19
51
  subType: z.ZodOptional<z.ZodString>;
20
- }, "strip", z.ZodTypeAny, {
21
- name: string;
22
- type: FieldType;
23
- optional?: boolean | undefined;
24
- description?: string | undefined;
25
- defaultValue?: any;
26
- isArray?: boolean | undefined;
27
- subType?: string | undefined;
28
- }, {
29
- name: string;
30
- type: FieldType;
31
- optional?: boolean | undefined;
32
- description?: string | undefined;
33
- defaultValue?: any;
34
- isArray?: boolean | undefined;
35
- subType?: string | undefined;
36
- }>;
37
- export type IField = z.infer<typeof fieldSchema>;
52
+ fields: z.ZodOptional<z.ZodLazy<z.ZodArray<z.ZodType<IField>>>>;
53
+ enumValues: z.ZodOptional<z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
54
+ }, "strip", z.ZodTypeAny, IField, IField>;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.fieldSchema = exports.FieldType = void 0;
4
4
  const zod_1 = require("zod");
5
5
  //export type FieldType = 'id' | 'string' | 'number' | 'boolean' | 'Date' | 'object' | 'blob' | 'any';
6
+ /** The value types a Peers IR field can have. */
6
7
  var FieldType;
7
8
  (function (FieldType) {
8
9
  FieldType["id"] = "id";
@@ -14,6 +15,7 @@ var FieldType;
14
15
  FieldType["blob"] = "blob";
15
16
  FieldType["any"] = "any";
16
17
  })(FieldType || (exports.FieldType = FieldType = {}));
18
+ /** Zod schema for {@link IField}, used where rows store field lists. */
17
19
  exports.fieldSchema = zod_1.z.object({
18
20
  name: zod_1.z.string(),
19
21
  description: zod_1.z.string().optional(),
@@ -21,6 +23,7 @@ exports.fieldSchema = zod_1.z.object({
21
23
  defaultValue: zod_1.z.any().optional(),
22
24
  optional: zod_1.z.boolean().optional(),
23
25
  isArray: zod_1.z.boolean().optional(),
24
- // enumValues: z.array(z.string()).optional(),
25
26
  subType: zod_1.z.string().optional(),
27
+ fields: zod_1.z.lazy(() => zod_1.z.array(exports.fieldSchema)).optional(),
28
+ enumValues: zod_1.z.array(zod_1.z.union([zod_1.z.string(), zod_1.z.number(), zod_1.z.boolean()])).optional(),
26
29
  });
@@ -1,8 +1,17 @@
1
1
  import { z } from "zod";
2
2
  export { z } from "zod";
3
+ /** Any plain object, with every key kept. */
3
4
  export declare const zodAnyObject: z.ZodObject<{}, "strip", z.ZodAny, z.objectOutputType<{}, z.ZodAny, "strip">, z.objectInputType<{}, z.ZodAny, "strip">>;
5
+ /** Any plain object or any array. */
4
6
  export declare const zodAnyObjectOrArray: z.ZodUnion<[z.ZodObject<{}, "strip", z.ZodAny, z.objectOutputType<{}, z.ZodAny, "strip">, z.objectInputType<{}, z.ZodAny, "strip">>, z.ZodArray<z.ZodAny, "many">]>;
7
+ /**
8
+ * A 25-character peer id from `newid()`. Marked as {@link FieldType.id}, so `schemaToFields`
9
+ * records the field as an id.
10
+ */
5
11
  export declare const zodPeerId: z.ZodEffects<z.ZodString, string, string>;
12
+ /** Any zod object schema. */
6
13
  export type ZodObjectType = z.ZodObject<any>;
14
+ /** Any zod array schema. */
7
15
  export type ZodArrayType = z.ZodArray<any>;
16
+ /** Any zod object or array schema. */
8
17
  export type ZodAnyObjectOrArrayType = ZodArrayType | ZodObjectType;
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.zodPeerId = exports.zodAnyObjectOrArray = exports.zodAnyObject = exports.z = void 0;
4
4
  const zod_1 = require("zod");
5
- const utils_1 = require("../utils");
5
+ const peer_id_1 = require("../peer-id");
6
6
  const field_type_1 = require("./field-type");
7
7
  // Re-export zod so consumers import it via peers-sdk. This guarantees every
8
8
  // schema in the graph is built with the same zod instance that peers-sdk's
@@ -10,10 +10,16 @@ const field_type_1 = require("./field-type");
10
10
  // matters because those helpers reach into zod internals (`_def`, shape).
11
11
  var zod_2 = require("zod");
12
12
  Object.defineProperty(exports, "z", { enumerable: true, get: function () { return zod_2.z; } });
13
+ /** Any plain object, with every key kept. */
13
14
  exports.zodAnyObject = zod_1.z.object({}).catchall(zod_1.z.any());
15
+ /** Any plain object or any array. */
14
16
  exports.zodAnyObjectOrArray = zod_1.z.union([exports.zodAnyObject, zod_1.z.array(zod_1.z.any())]);
17
+ /**
18
+ * A 25-character peer id from `newid()`. Marked as {@link FieldType.id}, so `schemaToFields`
19
+ * records the field as an id.
20
+ */
15
21
  exports.zodPeerId = zod_1.z.string().refine((value) => {
16
- return (0, utils_1.isid)(value);
22
+ return (0, peer_id_1.isid)(value);
17
23
  }, {
18
24
  message: "Invalid peer id, it must be a 25 character alphanumeric string",
19
25
  });
package/dist/utils.d.ts CHANGED
@@ -1,30 +1,6 @@
1
1
  import type { ITableMetaData } from "./data/orm";
2
2
  import { type IDeviceConnection } from "./types/peer-device";
3
- /** Builds a random string of base-36 characters with unbiased distribution (tweetnacl-backed). */
4
- export declare function cryptoRandomString(length: number): string;
5
- /**
6
- * Creates a new 25-character peer id: base-36 time prefix (10 chars) + random suffix (15 chars).
7
- * Sort order correlates with creation time. Use {@link isid} to validate user-provided values.
8
- */
9
- export declare function newid(): string;
10
- /** Lexicographic lower bound for valid peer ids (epoch). */
11
- export declare const MIN_ID: string;
12
- /** Lexicographic upper bound for valid peer ids (far future). */
13
- export declare const MAX_ID: string;
14
- /**
15
- * Returns whether `id` is a 25-char base-36 string whose embedded time is not in the future (5 min skew allowed).
16
- * @param id Candidate id (coerced to string)
17
- */
18
- export declare function isid(id: any): boolean;
19
- /** Returns the `Date.now()`-compatible millisecond time encoded in the first 10 base-36 characters of a peer id. */
20
- export declare function idTime(id: string): number;
21
- /** Interprets a peer id’s time prefix as a `Date` (see {@link idTime}). */
22
- export declare function idDate(id: string): Date;
23
- /**
24
- * Returns the 15-character random suffix of a peer id as two base-36 integers so large values stay exact in JS.
25
- * @param id A 25-character peer id from {@link newid}
26
- */
27
- export declare function idRandNums(id: string): [number, number];
3
+ export { cryptoRandomString, idDate, idRandNums, idTime, isid, MAX_ID, MIN_ID, newid, } from "./peer-id";
28
4
  /** Fast non-cryptographic 32-bit string hash (sign-insensitive) for display keys and bucketing. */
29
5
  export declare function simpleHash(str: string): number;
30
6
  /** Like TypeScript’s `Partial` for specific keys: required keys in `K` become optional. */
@@ -61,6 +37,23 @@ export declare function deterministicTableId(identity: string): string;
61
37
  * @param id Peer ID to inspect.
62
38
  */
63
39
  export declare function isDeterministicTableId(id: string): boolean;
40
+ /**
41
+ * The ID of a user's membership row in a group, the same on every device.
42
+ *
43
+ * Several of an admin's devices can approve the same join at once; with this ID they write one
44
+ * row, which merges as an ordinary update, instead of rows that collide on the
45
+ * `(groupId, userId)` unique index.
46
+ *
47
+ * @param groupId The group.
48
+ * @param userId The member.
49
+ */
50
+ export declare function deterministicGroupMemberId(groupId: string, userId: string): string;
51
+ /**
52
+ * Return whether a peer ID was generated by {@link deterministicGroupMemberId}.
53
+ *
54
+ * @param id Peer ID to inspect.
55
+ */
56
+ export declare function isDeterministicGroupMemberId(id: string): boolean;
64
57
  /** Resolves after `ms` milliseconds (0 yields a microtask-yield on most runtimes). */
65
58
  export declare function sleep(ms?: number): Promise<void>;
66
59
  /** Turns `fooBar` into `Foo Bar` for labels (also replaces `_` with space). */
@@ -117,6 +110,8 @@ export declare function getTimestamp(): number;
117
110
  * @param opts.timeout Clamped timeout; defaults to {@link PeerDeviceConsts} min/max
118
111
  * @param opts.retriesOnError Max error retries
119
112
  * @param opts.retriesOnTimeout Max “timed out” races before giving up
113
+ * @param opts.shouldRetry Return false for a terminal error that must be reported immediately.
114
+ * @param opts.logFailure Log exhausted error retries; defaults to `console.error`.
120
115
  * @throws Last error, timeout exhaustion, or if the connection is closed
121
116
  */
122
117
  export declare function retryOnErrorOrTimeout<T>({ fn, connection, opts, }: {
@@ -126,6 +121,8 @@ export declare function retryOnErrorOrTimeout<T>({ fn, connection, opts, }: {
126
121
  timeout?: number;
127
122
  retriesOnError?: number;
128
123
  retriesOnTimeout?: number;
124
+ shouldRetry?: (error: unknown) => boolean;
125
+ logFailure?: (message: string, error: unknown) => void;
129
126
  };
130
127
  }): Promise<T>;
131
128
  /**
package/dist/utils.js CHANGED
@@ -1,18 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TIMESTAMP_REANCHOR_THRESHOLD_MS = exports.moneyFormatter = exports.MAX_ID = exports.MIN_ID = void 0;
4
- exports.cryptoRandomString = cryptoRandomString;
5
- exports.newid = newid;
6
- exports.isid = isid;
7
- exports.idTime = idTime;
8
- exports.idDate = idDate;
9
- exports.idRandNums = idRandNums;
3
+ exports.TIMESTAMP_REANCHOR_THRESHOLD_MS = exports.moneyFormatter = exports.newid = exports.MIN_ID = exports.MAX_ID = exports.isid = exports.idTime = exports.idRandNums = exports.idDate = exports.cryptoRandomString = void 0;
10
4
  exports.simpleHash = simpleHash;
11
5
  exports.simpleObjectHash = simpleObjectHash;
12
6
  exports.deterministicPvarId = deterministicPvarId;
13
7
  exports.isDeterministicPvarId = isDeterministicPvarId;
14
8
  exports.deterministicTableId = deterministicTableId;
15
9
  exports.isDeterministicTableId = isDeterministicTableId;
10
+ exports.deterministicGroupMemberId = deterministicGroupMemberId;
11
+ exports.isDeterministicGroupMemberId = isDeterministicGroupMemberId;
16
12
  exports.sleep = sleep;
17
13
  exports.camelCaseToSpaces = camelCaseToSpaces;
18
14
  exports.camelCaseToHyphens = camelCaseToHyphens;
@@ -27,89 +23,15 @@ const _ = require("lodash");
27
23
  const nacl = require("tweetnacl");
28
24
  const peer_device_1 = require("./types/peer-device");
29
25
  const stableStringify = require("fast-json-stable-stringify");
30
- /** Builds a random string of base-36 characters with unbiased distribution (tweetnacl-backed). */
31
- function cryptoRandomString(length) {
32
- let s = "";
33
- let r = nacl.randomBytes(length + 6);
34
- let i = 0;
35
- while (s.length < length) {
36
- if (i >= r.length) {
37
- r = nacl.randomBytes(length - s.length);
38
- i = 0;
39
- }
40
- const n = r[i];
41
- i++;
42
- if (n >= 252) {
43
- // we don't want to bias the characters generated so only consider the range of numbers that spread evenly between factors of 36
44
- // we exclude 252, 253, 254, and 255 because it would bias towards '0', '1', '2', and '3' respectively
45
- continue;
46
- }
47
- s += (n % 36).toString(36);
48
- }
49
- return s.substring(0, length);
50
- }
51
- /**
52
- * Creates a new 25-character peer id: base-36 time prefix (10 chars) + random suffix (15 chars).
53
- * Sort order correlates with creation time. Use {@link isid} to validate user-provided values.
54
- */
55
- function newid() {
56
- // modeled after mongo ids, timestamp + counter + random but we're going to skip counter and have bigger random
57
- // people can switch to using a counter + random later if they want (or do something else with the random part).
58
- // This id is going to be a 128 bit number so it fits naturally into memory.
59
- // 2 ** 128 ~= 3.4e38 is the maximum it can be
60
- // For efficient transmission, storage, presentation, and usage we're going to represent this number with base 36 [0-9a-z]
61
- // (3.4e38).toString(36) == "f55n5nmuuaw00000000000000", this is our max in base 36, 25 characters
62
- // Date.now() =~ 1625282091498
63
- // but we're dealing in base 36 not base 10
64
- // (1625282091498).toString(36) == "kqn6zrvu" (length 8)
65
- // Number.parseInt("zzzzzzzz", 36) == 2821109907455 == year 2059. Way too soon so going up to 9 characters
66
- // Number.parseInt("zzzzzzzzz", 36) == 101559956668415 == year 5188.
67
- // but the max size of 9 left most can only be "f55n5nmuu" == 42720753566838 == year 3323. That could still work but only ~ 1300 years
68
- // that leaves 16 characters to represent a random number to prevent collisions each millisecond, 36 ** 16 ~= 8e24
69
- // Allocating one more character to the time and one less to the random number to avoid hitting max time in any conceivable future
70
- // now our max time is new Date(Number.parseInt('f55n5nmuua',36)) == +050705-08-09T23:40:06.178Z. 50k years from now.
71
- // We still have 15 chars for our random number. 36 ** 15 ~= 2e23
72
- // That is still a very large number and the chance of a collision should be so small as to be effectively globally unique
73
- // and certainly unique in any group's database.
74
- const time = Date.now().toString(36).padStart(10, "0"); // e.g: "00kq6xh45f", length == 10
75
- return time + cryptoRandomString(15);
76
- }
77
- /** Lexicographic lower bound for valid peer ids (epoch). */
78
- exports.MIN_ID = "0".repeat(25); // 1970
79
- /** Lexicographic upper bound for valid peer ids (far future). */
80
- exports.MAX_ID = `f55n5nmuua${"z".repeat(15)}`; // 50k years from now
81
- /**
82
- * Returns whether `id` is a 25-char base-36 string whose embedded time is not in the future (5 min skew allowed).
83
- * @param id Candidate id (coerced to string)
84
- */
85
- function isid(id) {
86
- id = String(id);
87
- const idMatch = Boolean(/^[0-9a-z]{25}$/i.exec(id));
88
- if (idMatch) {
89
- // only consider ids valid if they are less than current time (with 5 min buffer)
90
- // This is to prevent people from making ids with times in the future to get them to always show up as the "newest" item and/or prematurely claim ids
91
- return idTime(id) < Date.now() + 300000;
92
- }
93
- return false;
94
- }
95
- /** Returns the `Date.now()`-compatible millisecond time encoded in the first 10 base-36 characters of a peer id. */
96
- function idTime(id) {
97
- const time36 = id.substr(0, 10);
98
- return Number.parseInt(time36, 36);
99
- }
100
- /** Interprets a peer id’s time prefix as a `Date` (see {@link idTime}). */
101
- function idDate(id) {
102
- return new Date(idTime(id));
103
- }
104
- /**
105
- * Returns the 15-character random suffix of a peer id as two base-36 integers so large values stay exact in JS.
106
- * @param id A 25-character peer id from {@link newid}
107
- */
108
- function idRandNums(id) {
109
- const numPart1 = id.substr(10, 8);
110
- const numPart2 = id.substr(18, 7);
111
- return [Number.parseInt(numPart1, 36), Number.parseInt(numPart2, 36)];
112
- }
26
+ var peer_id_1 = require("./peer-id");
27
+ Object.defineProperty(exports, "cryptoRandomString", { enumerable: true, get: function () { return peer_id_1.cryptoRandomString; } });
28
+ Object.defineProperty(exports, "idDate", { enumerable: true, get: function () { return peer_id_1.idDate; } });
29
+ Object.defineProperty(exports, "idRandNums", { enumerable: true, get: function () { return peer_id_1.idRandNums; } });
30
+ Object.defineProperty(exports, "idTime", { enumerable: true, get: function () { return peer_id_1.idTime; } });
31
+ Object.defineProperty(exports, "isid", { enumerable: true, get: function () { return peer_id_1.isid; } });
32
+ Object.defineProperty(exports, "MAX_ID", { enumerable: true, get: function () { return peer_id_1.MAX_ID; } });
33
+ Object.defineProperty(exports, "MIN_ID", { enumerable: true, get: function () { return peer_id_1.MIN_ID; } });
34
+ Object.defineProperty(exports, "newid", { enumerable: true, get: function () { return peer_id_1.newid; } });
113
35
  /** Fast non-cryptographic 32-bit string hash (sign-insensitive) for display keys and bucketing. */
114
36
  function simpleHash(str) {
115
37
  let hash = 0;
@@ -128,6 +50,8 @@ function simpleObjectHash(obj) {
128
50
  const DETERMINISTIC_PVAR_PREFIX = "000pvar0";
129
51
  /** Well-known prefix for deterministic package-table IDs. */
130
52
  const DETERMINISTIC_TABLE_PREFIX = "000tbl00";
53
+ /** Well-known prefix for deterministic group-membership IDs. */
54
+ const DETERMINISTIC_GROUP_MEMBER_PREFIX = "000gmbr0";
131
55
  /**
132
56
  * Converts a byte array to a base-36 string of the requested length, using unbiased modular reduction.
133
57
  * Bytes >= 252 are skipped to avoid bias (same technique as {@link cryptoRandomString}).
@@ -185,6 +109,27 @@ function deterministicTableId(identity) {
185
109
  function isDeterministicTableId(id) {
186
110
  return id.startsWith(DETERMINISTIC_TABLE_PREFIX);
187
111
  }
112
+ /**
113
+ * The ID of a user's membership row in a group, the same on every device.
114
+ *
115
+ * Several of an admin's devices can approve the same join at once; with this ID they write one
116
+ * row, which merges as an ordinary update, instead of rows that collide on the
117
+ * `(groupId, userId)` unique index.
118
+ *
119
+ * @param groupId The group.
120
+ * @param userId The member.
121
+ */
122
+ function deterministicGroupMemberId(groupId, userId) {
123
+ return deterministicPeerId(DETERMINISTIC_GROUP_MEMBER_PREFIX, `${groupId}:${userId}`);
124
+ }
125
+ /**
126
+ * Return whether a peer ID was generated by {@link deterministicGroupMemberId}.
127
+ *
128
+ * @param id Peer ID to inspect.
129
+ */
130
+ function isDeterministicGroupMemberId(id) {
131
+ return id.startsWith(DETERMINISTIC_GROUP_MEMBER_PREFIX);
132
+ }
188
133
  /** Resolves after `ms` milliseconds (0 yields a microtask-yield on most runtimes). */
189
134
  function sleep(ms = 0) {
190
135
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -323,6 +268,8 @@ function getTimestamp() {
323
268
  * @param opts.timeout Clamped timeout; defaults to {@link PeerDeviceConsts} min/max
324
269
  * @param opts.retriesOnError Max error retries
325
270
  * @param opts.retriesOnTimeout Max “timed out” races before giving up
271
+ * @param opts.shouldRetry Return false for a terminal error that must be reported immediately.
272
+ * @param opts.logFailure Log exhausted error retries; defaults to `console.error`.
326
273
  * @throws Last error, timeout exhaustion, or if the connection is closed
327
274
  */
328
275
  async function retryOnErrorOrTimeout({ fn, connection, opts = {}, }) {
@@ -338,8 +285,8 @@ async function retryOnErrorOrTimeout({ fn, connection, opts = {}, }) {
338
285
  let resultSet = false;
339
286
  let finalError;
340
287
  while (errorCount <= retriesOnError && timeoutCount <= retriesOnTimeout) {
288
+ let sleepTimeout = null;
341
289
  try {
342
- let sleepTimeout = null;
343
290
  const sleepPromise = new Promise((resolve) => {
344
291
  sleepTimeout = setTimeout(() => {
345
292
  resolve();
@@ -372,14 +319,20 @@ async function retryOnErrorOrTimeout({ fn, connection, opts = {}, }) {
372
319
  finalError = error;
373
320
  connection.errorRate = connection.errorRate * 0.9 + 0.1;
374
321
  errorCount++;
322
+ if (opts.shouldRetry?.(error) === false)
323
+ throw error;
375
324
  if (connection.closed) {
376
325
  throw new Error(`connection has been closed`);
377
326
  }
378
327
  }
328
+ finally {
329
+ if (sleepTimeout !== null)
330
+ clearTimeout(sleepTimeout);
331
+ }
379
332
  console.warn(`Call failed, retrying: timeout is ${timeout}ms, errorCount: ${errorCount} of ${retriesOnError}, timeoutCount: ${timeoutCount} of ${retriesOnTimeout + 1}. Last error:`, finalError);
380
333
  }
381
334
  if (finalError) {
382
- console.error(`Retry failed after ${errorCount} errors and ${timeoutCount} timeouts. Last error:`, finalError);
335
+ (opts.logFailure ?? console.error)(`Retry failed after ${errorCount} errors and ${timeoutCount} timeouts. Last error:`, finalError);
383
336
  throw finalError;
384
337
  }
385
338
  else {
@@ -2,6 +2,25 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const lodash_1 = require("lodash");
4
4
  const utils_1 = require("./utils");
5
+ describe("retryOnErrorOrTimeout", () => {
6
+ it("reports a terminal error after one call and clears its timeout", async () => {
7
+ jest.useFakeTimers();
8
+ const error = new Error("No handler registered for event: listChanges_group");
9
+ const fn = jest.fn().mockRejectedValue(error);
10
+ try {
11
+ await expect((0, utils_1.retryOnErrorOrTimeout)({
12
+ fn,
13
+ connection: { deviceId: (0, utils_1.newid)(), latencyMs: 1, errorRate: 0, timestampLastApplied: 0 },
14
+ opts: { shouldRetry: () => false },
15
+ })).rejects.toBe(error);
16
+ expect(fn).toHaveBeenCalledTimes(1);
17
+ expect(jest.getTimerCount()).toBe(0);
18
+ }
19
+ finally {
20
+ jest.useRealTimers();
21
+ }
22
+ });
23
+ });
5
24
  describe("readResponseTextCapped", () => {
6
25
  it("returns a body within the cap and refuses one over it", async () => {
7
26
  await expect((0, utils_1.readResponseTextCapped)(new Response("hello"), 5)).resolves.toBe("hello");
@@ -132,6 +151,23 @@ describe("newid", () => {
132
151
  });
133
152
  });
134
153
  });
154
+ describe("deterministicGroupMemberId", () => {
155
+ it("preserves the membership identity protocol across SDK versions", () => {
156
+ expect((0, utils_1.deterministicGroupMemberId)("00000000000000000000000001", "00000000000000000000000002")).toBe("000gmbr0jzkpw7vuc2ew84u8c");
157
+ });
158
+ it("is a stable peer id per group and user", () => {
159
+ const groupId = (0, utils_1.newid)();
160
+ const userId = (0, utils_1.newid)();
161
+ const id = (0, utils_1.deterministicGroupMemberId)(groupId, userId);
162
+ expect(id).toMatch(/^[a-z0-9]{25}$/);
163
+ expect((0, utils_1.isid)(id)).toBe(true);
164
+ expect((0, utils_1.isDeterministicGroupMemberId)(id)).toBe(true);
165
+ expect((0, utils_1.deterministicGroupMemberId)(groupId, userId)).toBe(id);
166
+ expect((0, utils_1.deterministicGroupMemberId)(groupId, (0, utils_1.newid)())).not.toBe(id);
167
+ expect((0, utils_1.deterministicGroupMemberId)(userId, groupId)).not.toBe(id);
168
+ expect((0, utils_1.isDeterministicGroupMemberId)((0, utils_1.newid)())).toBe(false);
169
+ });
170
+ });
135
171
  describe("debounceByArgs", () => {
136
172
  beforeEach(() => {
137
173
  jest.useFakeTimers();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.26.6",
3
+ "version": "0.27.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"
@@ -9,8 +9,14 @@
9
9
  "license": "MIT",
10
10
  "main": "dist/index.js",
11
11
  "types": "dist/index.d.ts",
12
+ "bin": {
13
+ "peers-package": "bin/peers-package.js"
14
+ },
12
15
  "files": [
13
- "dist"
16
+ "dist",
17
+ "define",
18
+ "testing",
19
+ "bin"
14
20
  ],
15
21
  "publishConfig": {
16
22
  "access": "public"
@@ -61,6 +67,7 @@
61
67
  "@types/moment-timezone": "^0.5.30",
62
68
  "@types/node": "^22.4.0",
63
69
  "better-sqlite3": "^11.8.1",
70
+ "esbuild": "^0.28.2",
64
71
  "jest": "^29.7.0",
65
72
  "ts-jest": "^29.2.5",
66
73
  "typescript": "^5.8.3"
@@ -0,0 +1,4 @@
1
+ {
2
+ "main": "../dist/testing/index.js",
3
+ "types": "../dist/testing/index.d.ts"
4
+ }