@rei-standard/amsg-client 2.9.0-next.2 → 2.9.0-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -597,6 +597,95 @@ var ReiClient = class {
597
597
  data: decrypted
598
598
  };
599
599
  }
600
+ // ─── Client state (single-user cloud mirror) ────────────────────
601
+ /**
602
+ * Batch-upsert client-state entries (single-user worker,
603
+ * amsg-server 2.6.0+ `/client-state`).
604
+ *
605
+ * The worker keeps one live copy per (namespace, key); this client is
606
+ * the only writer. Send everything that changed in ONE call — e.g.
607
+ * inside the few-seconds window before iOS backgrounds the page — the
608
+ * server upserts the whole batch in a single DB round trip. Upserts
609
+ * are last-write-wins on `updatedAt`: entries older than the stored
610
+ * row are skipped, so re-sending a stale batch is harmless.
611
+ *
612
+ * The payload is encrypted like every other amsg-server call
613
+ * (requires `init()`); the worker re-encrypts each value at rest
614
+ * under the per-user key.
615
+ *
616
+ * @param {Array<{ namespace: string, key: string, value: string, updatedAt: number }>} entries
617
+ * - `value`: pre-serialized string (the SDK does not stringify it for you).
618
+ * - `updatedAt`: epoch milliseconds.
619
+ * @returns {Promise<Object>} `{ success, data?: { upserted, skipped }, error? }`
620
+ */
621
+ async putClientState(entries) {
622
+ if (!Array.isArray(entries) || entries.length === 0) {
623
+ throw new TypeError("[rei-standard-amsg-client] entries must be a non-empty array");
624
+ }
625
+ const json = JSON.stringify({ entries });
626
+ this._assertPayloadSize(json, "putClientState");
627
+ const encrypted = await this._encrypt(json);
628
+ const res = await fetch(`${this._baseUrl}/client-state`, {
629
+ method: "PUT",
630
+ headers: this._withServerToken({
631
+ "Content-Type": "application/json",
632
+ "X-User-Id": this._userId,
633
+ "X-Payload-Encrypted": "true",
634
+ "X-Encryption-Version": "1"
635
+ }),
636
+ body: JSON.stringify(encrypted)
637
+ });
638
+ return res.json();
639
+ }
640
+ /**
641
+ * Read every entry of one client-state namespace.
642
+ *
643
+ * The response rides the encrypted-response envelope (same as
644
+ * `listMessages`); this method decrypts it and returns plaintext
645
+ * values.
646
+ *
647
+ * @param {string} namespace
648
+ * @returns {Promise<Object>} `{ success, data?: { namespace, entries }, error? }`
649
+ * where `entries` is `Array<{ namespace, key, value, updatedAt }>`.
650
+ */
651
+ async getClientState(namespace) {
652
+ if (typeof namespace !== "string" || !namespace.trim()) {
653
+ throw new TypeError("[rei-standard-amsg-client] namespace must be a non-empty string");
654
+ }
655
+ const res = await fetch(
656
+ `${this._baseUrl}/client-state?namespace=${encodeURIComponent(namespace)}`,
657
+ {
658
+ method: "GET",
659
+ headers: this._withServerToken({
660
+ "X-User-Id": this._userId,
661
+ "X-Response-Encrypted": "true",
662
+ "X-Encryption-Version": "1"
663
+ })
664
+ }
665
+ );
666
+ const json = await res.json();
667
+ if (!json?.success || json?.encrypted !== true) return json;
668
+ const decrypted = await this._decrypt(json.data);
669
+ return {
670
+ success: true,
671
+ encrypted: true,
672
+ version: json.version || 1,
673
+ data: decrypted
674
+ };
675
+ }
676
+ /**
677
+ * Wipe every client-state entry of this user, across all namespaces —
678
+ * e.g. behind a "clear cloud state" settings action.
679
+ *
680
+ * @returns {Promise<Object>} `{ success, data?: { deleted }, error? }`
681
+ */
682
+ async clearClientState() {
683
+ const res = await fetch(`${this._baseUrl}/client-state`, {
684
+ method: "DELETE",
685
+ headers: this._withServerToken({ "X-User-Id": this._userId })
686
+ });
687
+ return res.json();
688
+ }
600
689
  // ─── Push Subscription ──────────────────────────────────────────
601
690
  /**
602
691
  * Subscribe to Web Push notifications.
package/dist/index.d.cts CHANGED
@@ -992,6 +992,104 @@ class ReiClient {
992
992
  };
993
993
  }
994
994
 
995
+ // ─── Client state (single-user cloud mirror) ────────────────────
996
+
997
+ /**
998
+ * Batch-upsert client-state entries (single-user worker,
999
+ * amsg-server 2.6.0+ `/client-state`).
1000
+ *
1001
+ * The worker keeps one live copy per (namespace, key); this client is
1002
+ * the only writer. Send everything that changed in ONE call — e.g.
1003
+ * inside the few-seconds window before iOS backgrounds the page — the
1004
+ * server upserts the whole batch in a single DB round trip. Upserts
1005
+ * are last-write-wins on `updatedAt`: entries older than the stored
1006
+ * row are skipped, so re-sending a stale batch is harmless.
1007
+ *
1008
+ * The payload is encrypted like every other amsg-server call
1009
+ * (requires `init()`); the worker re-encrypts each value at rest
1010
+ * under the per-user key.
1011
+ *
1012
+ * @param {Array<{ namespace: string, key: string, value: string, updatedAt: number }>} entries
1013
+ * - `value`: pre-serialized string (the SDK does not stringify it for you).
1014
+ * - `updatedAt`: epoch milliseconds.
1015
+ * @returns {Promise<Object>} `{ success, data?: { upserted, skipped }, error? }`
1016
+ */
1017
+ async putClientState(entries) {
1018
+ if (!Array.isArray(entries) || entries.length === 0) {
1019
+ throw new TypeError('[rei-standard-amsg-client] entries must be a non-empty array');
1020
+ }
1021
+ const json = JSON.stringify({ entries });
1022
+ this._assertPayloadSize(json, 'putClientState');
1023
+ const encrypted = await this._encrypt(json);
1024
+
1025
+ const res = await fetch(`${this._baseUrl}/client-state`, {
1026
+ method: 'PUT',
1027
+ headers: this._withServerToken({
1028
+ 'Content-Type': 'application/json',
1029
+ 'X-User-Id': this._userId,
1030
+ 'X-Payload-Encrypted': 'true',
1031
+ 'X-Encryption-Version': '1'
1032
+ }),
1033
+ body: JSON.stringify(encrypted)
1034
+ });
1035
+
1036
+ return res.json();
1037
+ }
1038
+
1039
+ /**
1040
+ * Read every entry of one client-state namespace.
1041
+ *
1042
+ * The response rides the encrypted-response envelope (same as
1043
+ * `listMessages`); this method decrypts it and returns plaintext
1044
+ * values.
1045
+ *
1046
+ * @param {string} namespace
1047
+ * @returns {Promise<Object>} `{ success, data?: { namespace, entries }, error? }`
1048
+ * where `entries` is `Array<{ namespace, key, value, updatedAt }>`.
1049
+ */
1050
+ async getClientState(namespace) {
1051
+ if (typeof namespace !== 'string' || !namespace.trim()) {
1052
+ throw new TypeError('[rei-standard-amsg-client] namespace must be a non-empty string');
1053
+ }
1054
+ const res = await fetch(
1055
+ `${this._baseUrl}/client-state?namespace=${encodeURIComponent(namespace)}`,
1056
+ {
1057
+ method: 'GET',
1058
+ headers: this._withServerToken({
1059
+ 'X-User-Id': this._userId,
1060
+ 'X-Response-Encrypted': 'true',
1061
+ 'X-Encryption-Version': '1'
1062
+ })
1063
+ }
1064
+ );
1065
+
1066
+ const json = await res.json();
1067
+ if (!json?.success || json?.encrypted !== true) return json;
1068
+
1069
+ const decrypted = await this._decrypt(json.data);
1070
+ return {
1071
+ success: true,
1072
+ encrypted: true,
1073
+ version: json.version || 1,
1074
+ data: decrypted
1075
+ };
1076
+ }
1077
+
1078
+ /**
1079
+ * Wipe every client-state entry of this user, across all namespaces —
1080
+ * e.g. behind a "clear cloud state" settings action.
1081
+ *
1082
+ * @returns {Promise<Object>} `{ success, data?: { deleted }, error? }`
1083
+ */
1084
+ async clearClientState() {
1085
+ const res = await fetch(`${this._baseUrl}/client-state`, {
1086
+ method: 'DELETE',
1087
+ headers: this._withServerToken({ 'X-User-Id': this._userId })
1088
+ });
1089
+
1090
+ return res.json();
1091
+ }
1092
+
995
1093
  // ─── Push Subscription ──────────────────────────────────────────
996
1094
 
997
1095
  /**
package/dist/index.d.ts CHANGED
@@ -992,6 +992,104 @@ class ReiClient {
992
992
  };
993
993
  }
994
994
 
995
+ // ─── Client state (single-user cloud mirror) ────────────────────
996
+
997
+ /**
998
+ * Batch-upsert client-state entries (single-user worker,
999
+ * amsg-server 2.6.0+ `/client-state`).
1000
+ *
1001
+ * The worker keeps one live copy per (namespace, key); this client is
1002
+ * the only writer. Send everything that changed in ONE call — e.g.
1003
+ * inside the few-seconds window before iOS backgrounds the page — the
1004
+ * server upserts the whole batch in a single DB round trip. Upserts
1005
+ * are last-write-wins on `updatedAt`: entries older than the stored
1006
+ * row are skipped, so re-sending a stale batch is harmless.
1007
+ *
1008
+ * The payload is encrypted like every other amsg-server call
1009
+ * (requires `init()`); the worker re-encrypts each value at rest
1010
+ * under the per-user key.
1011
+ *
1012
+ * @param {Array<{ namespace: string, key: string, value: string, updatedAt: number }>} entries
1013
+ * - `value`: pre-serialized string (the SDK does not stringify it for you).
1014
+ * - `updatedAt`: epoch milliseconds.
1015
+ * @returns {Promise<Object>} `{ success, data?: { upserted, skipped }, error? }`
1016
+ */
1017
+ async putClientState(entries) {
1018
+ if (!Array.isArray(entries) || entries.length === 0) {
1019
+ throw new TypeError('[rei-standard-amsg-client] entries must be a non-empty array');
1020
+ }
1021
+ const json = JSON.stringify({ entries });
1022
+ this._assertPayloadSize(json, 'putClientState');
1023
+ const encrypted = await this._encrypt(json);
1024
+
1025
+ const res = await fetch(`${this._baseUrl}/client-state`, {
1026
+ method: 'PUT',
1027
+ headers: this._withServerToken({
1028
+ 'Content-Type': 'application/json',
1029
+ 'X-User-Id': this._userId,
1030
+ 'X-Payload-Encrypted': 'true',
1031
+ 'X-Encryption-Version': '1'
1032
+ }),
1033
+ body: JSON.stringify(encrypted)
1034
+ });
1035
+
1036
+ return res.json();
1037
+ }
1038
+
1039
+ /**
1040
+ * Read every entry of one client-state namespace.
1041
+ *
1042
+ * The response rides the encrypted-response envelope (same as
1043
+ * `listMessages`); this method decrypts it and returns plaintext
1044
+ * values.
1045
+ *
1046
+ * @param {string} namespace
1047
+ * @returns {Promise<Object>} `{ success, data?: { namespace, entries }, error? }`
1048
+ * where `entries` is `Array<{ namespace, key, value, updatedAt }>`.
1049
+ */
1050
+ async getClientState(namespace) {
1051
+ if (typeof namespace !== 'string' || !namespace.trim()) {
1052
+ throw new TypeError('[rei-standard-amsg-client] namespace must be a non-empty string');
1053
+ }
1054
+ const res = await fetch(
1055
+ `${this._baseUrl}/client-state?namespace=${encodeURIComponent(namespace)}`,
1056
+ {
1057
+ method: 'GET',
1058
+ headers: this._withServerToken({
1059
+ 'X-User-Id': this._userId,
1060
+ 'X-Response-Encrypted': 'true',
1061
+ 'X-Encryption-Version': '1'
1062
+ })
1063
+ }
1064
+ );
1065
+
1066
+ const json = await res.json();
1067
+ if (!json?.success || json?.encrypted !== true) return json;
1068
+
1069
+ const decrypted = await this._decrypt(json.data);
1070
+ return {
1071
+ success: true,
1072
+ encrypted: true,
1073
+ version: json.version || 1,
1074
+ data: decrypted
1075
+ };
1076
+ }
1077
+
1078
+ /**
1079
+ * Wipe every client-state entry of this user, across all namespaces —
1080
+ * e.g. behind a "clear cloud state" settings action.
1081
+ *
1082
+ * @returns {Promise<Object>} `{ success, data?: { deleted }, error? }`
1083
+ */
1084
+ async clearClientState() {
1085
+ const res = await fetch(`${this._baseUrl}/client-state`, {
1086
+ method: 'DELETE',
1087
+ headers: this._withServerToken({ 'X-User-Id': this._userId })
1088
+ });
1089
+
1090
+ return res.json();
1091
+ }
1092
+
995
1093
  // ─── Push Subscription ──────────────────────────────────────────
996
1094
 
997
1095
  /**
package/dist/index.mjs CHANGED
@@ -575,6 +575,95 @@ var ReiClient = class {
575
575
  data: decrypted
576
576
  };
577
577
  }
578
+ // ─── Client state (single-user cloud mirror) ────────────────────
579
+ /**
580
+ * Batch-upsert client-state entries (single-user worker,
581
+ * amsg-server 2.6.0+ `/client-state`).
582
+ *
583
+ * The worker keeps one live copy per (namespace, key); this client is
584
+ * the only writer. Send everything that changed in ONE call — e.g.
585
+ * inside the few-seconds window before iOS backgrounds the page — the
586
+ * server upserts the whole batch in a single DB round trip. Upserts
587
+ * are last-write-wins on `updatedAt`: entries older than the stored
588
+ * row are skipped, so re-sending a stale batch is harmless.
589
+ *
590
+ * The payload is encrypted like every other amsg-server call
591
+ * (requires `init()`); the worker re-encrypts each value at rest
592
+ * under the per-user key.
593
+ *
594
+ * @param {Array<{ namespace: string, key: string, value: string, updatedAt: number }>} entries
595
+ * - `value`: pre-serialized string (the SDK does not stringify it for you).
596
+ * - `updatedAt`: epoch milliseconds.
597
+ * @returns {Promise<Object>} `{ success, data?: { upserted, skipped }, error? }`
598
+ */
599
+ async putClientState(entries) {
600
+ if (!Array.isArray(entries) || entries.length === 0) {
601
+ throw new TypeError("[rei-standard-amsg-client] entries must be a non-empty array");
602
+ }
603
+ const json = JSON.stringify({ entries });
604
+ this._assertPayloadSize(json, "putClientState");
605
+ const encrypted = await this._encrypt(json);
606
+ const res = await fetch(`${this._baseUrl}/client-state`, {
607
+ method: "PUT",
608
+ headers: this._withServerToken({
609
+ "Content-Type": "application/json",
610
+ "X-User-Id": this._userId,
611
+ "X-Payload-Encrypted": "true",
612
+ "X-Encryption-Version": "1"
613
+ }),
614
+ body: JSON.stringify(encrypted)
615
+ });
616
+ return res.json();
617
+ }
618
+ /**
619
+ * Read every entry of one client-state namespace.
620
+ *
621
+ * The response rides the encrypted-response envelope (same as
622
+ * `listMessages`); this method decrypts it and returns plaintext
623
+ * values.
624
+ *
625
+ * @param {string} namespace
626
+ * @returns {Promise<Object>} `{ success, data?: { namespace, entries }, error? }`
627
+ * where `entries` is `Array<{ namespace, key, value, updatedAt }>`.
628
+ */
629
+ async getClientState(namespace) {
630
+ if (typeof namespace !== "string" || !namespace.trim()) {
631
+ throw new TypeError("[rei-standard-amsg-client] namespace must be a non-empty string");
632
+ }
633
+ const res = await fetch(
634
+ `${this._baseUrl}/client-state?namespace=${encodeURIComponent(namespace)}`,
635
+ {
636
+ method: "GET",
637
+ headers: this._withServerToken({
638
+ "X-User-Id": this._userId,
639
+ "X-Response-Encrypted": "true",
640
+ "X-Encryption-Version": "1"
641
+ })
642
+ }
643
+ );
644
+ const json = await res.json();
645
+ if (!json?.success || json?.encrypted !== true) return json;
646
+ const decrypted = await this._decrypt(json.data);
647
+ return {
648
+ success: true,
649
+ encrypted: true,
650
+ version: json.version || 1,
651
+ data: decrypted
652
+ };
653
+ }
654
+ /**
655
+ * Wipe every client-state entry of this user, across all namespaces —
656
+ * e.g. behind a "clear cloud state" settings action.
657
+ *
658
+ * @returns {Promise<Object>} `{ success, data?: { deleted }, error? }`
659
+ */
660
+ async clearClientState() {
661
+ const res = await fetch(`${this._baseUrl}/client-state`, {
662
+ method: "DELETE",
663
+ headers: this._withServerToken({ "X-User-Id": this._userId })
664
+ });
665
+ return res.json();
666
+ }
578
667
  // ─── Push Subscription ──────────────────────────────────────────
579
668
  /**
580
669
  * Subscribe to Web Push notifications.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rei-standard/amsg-client",
3
- "version": "2.9.0-next.2",
3
+ "version": "2.9.0-next.3",
4
4
  "description": "ReiStandard Active Messaging browser client SDK — also re-exports shared push types, builders, and guards from @rei-standard/amsg-shared",
5
5
  "repository": {
6
6
  "type": "git",