@rei-standard/amsg-sw 2.0.1 → 2.1.0-next.0

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/README.md CHANGED
@@ -3,9 +3,53 @@
3
3
  `@rei-standard/amsg-sw` 是 ReiStandard 主动消息标准的 Service Worker 插件包,目标是让推送展示和离线重试“开箱即用”。
4
4
 
5
5
 
6
+ ## v2.1.0 — 按 kind 分发的客户端事件
7
+
8
+ 2.1.0 跟随 `@rei-standard/amsg-shared` 的三轴 push schema:每条 push 现在通过 `payload.messageKind`(`content` / `reasoning` / `tool_request` / `error`)区分内容类型。SW 在收到 push 后会做两件事:
9
+
10
+ 1. **永远** 通过 `postMessage` 把 payload 广播给所有受控窗口(包括 `includeUncontrolled: true` 的未受控窗口)。
11
+ 2. **仅当** `messageKind === 'content'` 或 payload 没有 `messageKind`(2.0.x 老 payload 的回退路径)时,才调用 `showNotification`。`reasoning` / `tool_request` / `error` 三种 kind 一律不弹通知——业务在 app 内通过 postMessage 通道自行渲染。
12
+
13
+ ### 新增导出 `REI_SW_EVENT`
14
+
15
+ 事件名由 SW 在每次广播时打在 `e.data.event` 上:
16
+
17
+ | 常量 | 字符串值 | 触发条件 |
18
+ |------|---------|---------|
19
+ | `REI_SW_EVENT.CONTENT_RECEIVED` | `'rei-amsg-content-received'` | `payload.messageKind === 'content'` |
20
+ | `REI_SW_EVENT.REASONING_RECEIVED` | `'rei-amsg-reasoning-received'` | `payload.messageKind === 'reasoning'` |
21
+ | `REI_SW_EVENT.TOOL_REQUEST_RECEIVED` | `'rei-amsg-tool-request-received'` | `payload.messageKind === 'tool_request'` |
22
+ | `REI_SW_EVENT.ERROR_RECEIVED` | `'rei-amsg-error-received'` | `payload.messageKind === 'error'` |
23
+ | `REI_SW_EVENT.UNKNOWN_RECEIVED` | `'rei-amsg-unknown-received'` | 缺 `messageKind`(2.0.x 老 payload / blob envelope) |
24
+
25
+ ### 客户端订阅示例
26
+
27
+ ```js
28
+ navigator.serviceWorker.addEventListener('message', (e) => {
29
+ if (e.data?.type !== 'REI_AMSG_PUSH') return;
30
+ switch (e.data.event) {
31
+ case 'rei-amsg-content-received': /* 渲染 app 内消息 */ break;
32
+ case 'rei-amsg-reasoning-received': /* 渲染思考中 UI */ break;
33
+ case 'rei-amsg-tool-request-received': /* 弹出工具执行确认 */ break;
34
+ case 'rei-amsg-error-received': /* 显示错误 toast */ break;
35
+ case 'rei-amsg-unknown-received': /* 2.0.x 老 payload 的兼容路径 */ break;
36
+ }
37
+ });
38
+ ```
39
+
40
+ ### Blob envelope
41
+
42
+ 当 `amsg-instant` 检测到 payload 超过 `maxInlineBytes` 时会改发 blob envelope `{ _blob: true, key, url, messageKind?, type? }`。SW **不会** 自动 fetch blob 内容(那是 client 的职责),但仍然会按 envelope 上的 `messageKind` 分发对应事件,让 client 知道有什么类型的内容即将到达,自己决定要不要拉取。Blob envelope 也只在 `messageKind === 'content'`(或缺失)时才渲染占位通知,与普通 push 行为一致。
43
+
44
+ ### 升级注意事项
45
+
46
+ - 想给 `reasoning` / `tool_request` / `error` 也弹通知的业务:必须自行在 app 内监听上面的 postMessage 事件、调 `Notification` 或 `registration.showNotification`。SW 默认不再为它们弹通知。
47
+ - 客户端代码继续兼容只有 `installReiSW` + `REI_SW_MESSAGE_TYPE`(队列)的 2.0.x 写法——新增导出不破坏既有 API。
48
+ - 想拿到 push 类型相关的 TS 类型:从 `@rei-standard/amsg-shared` 引 `AmsgPush` 等类型(本包通过 JSDoc 引用同一份类型)。
49
+
6
50
  ## 功能概览
7
51
 
8
- - 处理 `push` 事件:自动解析 payload 并展示通知
52
+ - 处理 `push` 事件:按 `messageKind` 三轴 schema 分发到客户端 + 仅 `content` 走 `showNotification`
9
53
  - 处理 `message` 事件:支持离线请求入队与主动冲刷队列
10
54
  - 处理 `sync` 事件:在网络恢复后自动重试队列请求
11
55
  - 使用 IndexedDB 存储待发送请求,避免页面关闭后丢失
@@ -97,8 +141,18 @@ export async function enqueueRequestToSW(requestPayload) {
97
141
  ## 导出 API(Exports)
98
142
 
99
143
  - `installReiSW`
144
+ - `REI_SW_EVENT` — 2.1.0 新增,按 kind 分发的客户端事件名
145
+ - `REI_AMSG_POSTMESSAGE_TYPE` — 2.1.0 新增,SW → client 广播信封的 `type` 字段(恒为 `'REI_AMSG_PUSH'`)
100
146
  - `REI_SW_MESSAGE_TYPE`
101
147
 
148
+ `REI_SW_EVENT` 包含(详见上文 v2.1.0 章节):
149
+
150
+ - `CONTENT_RECEIVED`
151
+ - `REASONING_RECEIVED`
152
+ - `TOOL_REQUEST_RECEIVED`
153
+ - `ERROR_RECEIVED`
154
+ - `UNKNOWN_RECEIVED`
155
+
102
156
  `REI_SW_MESSAGE_TYPE` 包含:
103
157
 
104
158
  - `ENQUEUE_REQUEST`
package/dist/index.cjs CHANGED
@@ -19,6 +19,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  // src/index.js
20
20
  var src_exports = {};
21
21
  __export(src_exports, {
22
+ REI_AMSG_POSTMESSAGE_TYPE: () => REI_AMSG_POSTMESSAGE_TYPE,
23
+ REI_SW_EVENT: () => REI_SW_EVENT,
22
24
  REI_SW_MESSAGE_TYPE: () => REI_SW_MESSAGE_TYPE,
23
25
  installReiSW: () => installReiSW
24
26
  });
@@ -27,6 +29,14 @@ var REI_SW_DB_NAME = "rei-sw";
27
29
  var REI_SW_DB_STORE = "request-outbox";
28
30
  var REI_SW_DB_VERSION = 1;
29
31
  var REI_SW_SYNC_TAG = "rei-sw-flush-request-outbox";
32
+ var REI_AMSG_POSTMESSAGE_TYPE = "REI_AMSG_PUSH";
33
+ var REI_SW_EVENT = Object.freeze({
34
+ CONTENT_RECEIVED: "rei-amsg-content-received",
35
+ REASONING_RECEIVED: "rei-amsg-reasoning-received",
36
+ TOOL_REQUEST_RECEIVED: "rei-amsg-tool-request-received",
37
+ ERROR_RECEIVED: "rei-amsg-error-received",
38
+ UNKNOWN_RECEIVED: "rei-amsg-unknown-received"
39
+ });
30
40
  var REI_SW_MESSAGE_TYPE = Object.freeze({
31
41
  ENQUEUE_REQUEST: "REI_ENQUEUE_REQUEST",
32
42
  FLUSH_QUEUE: "REI_FLUSH_QUEUE",
@@ -38,14 +48,21 @@ function installReiSW(sw, opts = {}) {
38
48
  sw.addEventListener("push", (event) => {
39
49
  const payload = readPushPayload(event);
40
50
  if (!payload) return;
41
- const notification = createNotificationFromPayload(payload, {
42
- defaultIcon,
43
- defaultBadge
44
- });
45
- if (!notification) return;
46
- event.waitUntil(
47
- sw.registration.showNotification(notification.title, notification.options)
48
- );
51
+ const eventName = resolveEventName(payload);
52
+ const shouldRenderNotification = isNotificationKind(payload);
53
+ const work = [dispatchPushToClients(sw, eventName, payload)];
54
+ if (shouldRenderNotification) {
55
+ const notification = createNotificationFromPayload(payload, {
56
+ defaultIcon,
57
+ defaultBadge
58
+ });
59
+ if (notification) {
60
+ work.push(
61
+ sw.registration.showNotification(notification.title, notification.options)
62
+ );
63
+ }
64
+ }
65
+ event.waitUntil(Promise.all(work));
49
66
  });
50
67
  sw.addEventListener("message", (event) => {
51
68
  const message = event.data;
@@ -65,6 +82,47 @@ function installReiSW(sw, opts = {}) {
65
82
  event.waitUntil(flushQueuedRequests(sw));
66
83
  });
67
84
  }
85
+ function resolveEventName(payload) {
86
+ const kind = payload && typeof payload === "object" ? payload.messageKind : void 0;
87
+ switch (kind) {
88
+ case "content":
89
+ return REI_SW_EVENT.CONTENT_RECEIVED;
90
+ case "reasoning":
91
+ return REI_SW_EVENT.REASONING_RECEIVED;
92
+ case "tool_request":
93
+ return REI_SW_EVENT.TOOL_REQUEST_RECEIVED;
94
+ case "error":
95
+ return REI_SW_EVENT.ERROR_RECEIVED;
96
+ default:
97
+ return REI_SW_EVENT.UNKNOWN_RECEIVED;
98
+ }
99
+ }
100
+ function isNotificationKind(payload) {
101
+ if (!payload || typeof payload !== "object") return false;
102
+ const kind = payload.messageKind;
103
+ if (kind === void 0 || kind === null) return true;
104
+ return kind === "content";
105
+ }
106
+ async function dispatchPushToClients(sw, eventName, payload) {
107
+ try {
108
+ const clientList = await sw.clients.matchAll({
109
+ type: "window",
110
+ includeUncontrolled: true
111
+ });
112
+ const envelope = {
113
+ type: REI_AMSG_POSTMESSAGE_TYPE,
114
+ event: eventName,
115
+ payload
116
+ };
117
+ for (const client of clientList) {
118
+ try {
119
+ client.postMessage(envelope);
120
+ } catch (_postError) {
121
+ }
122
+ }
123
+ } catch (_matchError) {
124
+ }
125
+ }
68
126
  function readPushPayload(event) {
69
127
  if (!event.data) return null;
70
128
  try {
package/dist/index.d.cts CHANGED
@@ -2,34 +2,79 @@
2
2
  * ReiStandard Service Worker helpers.
3
3
  *
4
4
  * Drop-in plugin for Service Workers that handles:
5
- * - Basic push payload -> notification rendering
6
- * - Offline request queueing and retry with Background Sync
5
+ * - Three-axis `push` payload dispatch keyed by `payload.messageKind`
6
+ * (see `@rei-standard/amsg-shared`). Every push is mirrored to every
7
+ * controlled client via `postMessage` under a per-kind event name.
8
+ * - Notification rendering for `messageKind: 'content'` (and legacy
9
+ * payloads without `messageKind`, for back-compat with 2.0.x
10
+ * producers).
11
+ * - Offline request queueing and retry with Background Sync.
7
12
  *
8
13
  * Notes:
9
14
  * - This plugin intentionally does not install `notificationclick`.
10
15
  * Main applications can implement their own click navigation logic.
16
+ * - `reasoning` / `tool_request` / `error` pushes are dispatched as
17
+ * `postMessage` events but **do not** trigger `showNotification` —
18
+ * apps render those in-app via the postMessage channel.
19
+ * - Blob envelopes (`{ _blob: true, key, url, messageKind? }`) are
20
+ * dispatched to clients verbatim. The SW never auto-fetches the
21
+ * blob body — that's the client's job.
11
22
  *
12
23
  * Usage (inside your sw.js):
13
- * import { installReiSW, REI_SW_MESSAGE_TYPE } from '@rei-standard/amsg-sw';
24
+ * import { installReiSW, REI_SW_EVENT, REI_SW_MESSAGE_TYPE } from '@rei-standard/amsg-sw';
14
25
  * installReiSW(self);
15
26
  *
16
27
  * Usage (inside your web app):
17
- * navigator.serviceWorker.controller?.postMessage({
18
- * type: REI_SW_MESSAGE_TYPE.ENQUEUE_REQUEST,
19
- * request: {
20
- * url: '/api/messages/send',
21
- * method: 'POST',
22
- * headers: { 'Content-Type': 'application/json' },
23
- * body: { text: 'hello' }
28
+ * navigator.serviceWorker.addEventListener('message', (e) => {
29
+ * if (e.data?.type !== 'REI_AMSG_PUSH') return;
30
+ * switch (e.data.event) {
31
+ * case REI_SW_EVENT.CONTENT_RECEIVED: // render in-app message
32
+ * case REI_SW_EVENT.REASONING_RECEIVED: // render thinking UI
33
+ * case REI_SW_EVENT.TOOL_REQUEST_RECEIVED: // prompt tool exec
34
+ * case REI_SW_EVENT.ERROR_RECEIVED: // show error toast
35
+ * case REI_SW_EVENT.UNKNOWN_RECEIVED: // legacy 2.0.x payload
24
36
  * }
25
37
  * });
26
38
  */
27
39
 
40
+ /**
41
+ * @typedef {import('@rei-standard/amsg-shared').AmsgPush} AmsgPush
42
+ * @typedef {import('@rei-standard/amsg-shared').ContentPush} ContentPush
43
+ * @typedef {import('@rei-standard/amsg-shared').ReasoningPush} ReasoningPush
44
+ * @typedef {import('@rei-standard/amsg-shared').ToolRequestPush} ToolRequestPush
45
+ * @typedef {import('@rei-standard/amsg-shared').ErrorPush} ErrorPush
46
+ */
47
+
28
48
  const REI_SW_DB_NAME = 'rei-sw';
29
49
  const REI_SW_DB_STORE = 'request-outbox';
30
50
  const REI_SW_DB_VERSION = 1;
31
51
  const REI_SW_SYNC_TAG = 'rei-sw-flush-request-outbox';
32
52
 
53
+ /**
54
+ * Wire-level message type for SW → client postMessage envelopes.
55
+ * Clients filter on `e.data.type === 'REI_AMSG_PUSH'` before reading
56
+ * `e.data.event` (which is one of {@link REI_SW_EVENT}'s values).
57
+ */
58
+ const REI_AMSG_POSTMESSAGE_TYPE = 'REI_AMSG_PUSH';
59
+
60
+ /**
61
+ * Per-kind event names dispatched to controlled clients. Each push the
62
+ * SW receives is mirrored to every window via
63
+ * `postMessage({ type: 'REI_AMSG_PUSH', event: <one of these>, payload })`.
64
+ *
65
+ * The mapping is keyed by `payload.messageKind`. Legacy payloads (and
66
+ * blob envelopes) without a `messageKind` field dispatch as
67
+ * {@link REI_SW_EVENT.UNKNOWN_RECEIVED} so apps can still handle 2.0.x
68
+ * producers during migration.
69
+ */
70
+ const REI_SW_EVENT = Object.freeze({
71
+ CONTENT_RECEIVED: 'rei-amsg-content-received',
72
+ REASONING_RECEIVED: 'rei-amsg-reasoning-received',
73
+ TOOL_REQUEST_RECEIVED: 'rei-amsg-tool-request-received',
74
+ ERROR_RECEIVED: 'rei-amsg-error-received',
75
+ UNKNOWN_RECEIVED: 'rei-amsg-unknown-received'
76
+ });
77
+
33
78
  const REI_SW_MESSAGE_TYPE = Object.freeze({
34
79
  ENQUEUE_REQUEST: 'REI_ENQUEUE_REQUEST',
35
80
  FLUSH_QUEUE: 'REI_FLUSH_QUEUE',
@@ -56,15 +101,25 @@ function installReiSW(sw, opts = {}) {
56
101
  const payload = readPushPayload(event);
57
102
  if (!payload) return;
58
103
 
59
- const notification = createNotificationFromPayload(payload, {
60
- defaultIcon,
61
- defaultBadge
62
- });
63
- if (!notification) return;
104
+ const eventName = resolveEventName(payload);
105
+ const shouldRenderNotification = isNotificationKind(payload);
106
+
107
+ /** @type {Array<Promise<unknown>>} */
108
+ const work = [dispatchPushToClients(sw, eventName, payload)];
109
+
110
+ if (shouldRenderNotification) {
111
+ const notification = createNotificationFromPayload(payload, {
112
+ defaultIcon,
113
+ defaultBadge
114
+ });
115
+ if (notification) {
116
+ work.push(
117
+ sw.registration.showNotification(notification.title, notification.options)
118
+ );
119
+ }
120
+ }
64
121
 
65
- event.waitUntil(
66
- sw.registration.showNotification(notification.title, notification.options)
67
- );
122
+ event.waitUntil(Promise.all(work));
68
123
  });
69
124
 
70
125
  sw.addEventListener('message', (event) => {
@@ -89,6 +144,85 @@ function installReiSW(sw, opts = {}) {
89
144
  });
90
145
  }
91
146
 
147
+ /**
148
+ * Map a parsed push payload to its corresponding per-kind event name.
149
+ * Falls back to `UNKNOWN_RECEIVED` for legacy 2.0.x payloads and blob
150
+ * envelopes without `messageKind`.
151
+ *
152
+ * @param {Record<string, unknown>} payload
153
+ * @returns {string}
154
+ */
155
+ function resolveEventName(payload) {
156
+ const kind = payload && typeof payload === 'object' ? payload.messageKind : undefined;
157
+ switch (kind) {
158
+ case 'content':
159
+ return REI_SW_EVENT.CONTENT_RECEIVED;
160
+ case 'reasoning':
161
+ return REI_SW_EVENT.REASONING_RECEIVED;
162
+ case 'tool_request':
163
+ return REI_SW_EVENT.TOOL_REQUEST_RECEIVED;
164
+ case 'error':
165
+ return REI_SW_EVENT.ERROR_RECEIVED;
166
+ default:
167
+ return REI_SW_EVENT.UNKNOWN_RECEIVED;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * True when the payload should trigger `showNotification`. Only
173
+ * `messageKind: 'content'` renders a notification; everything else
174
+ * (`reasoning`, `tool_request`, `error`) is dispatched silently so
175
+ * apps can render them in-app.
176
+ *
177
+ * Legacy payloads with no `messageKind` field still render a
178
+ * notification — that's the 2.0.x back-compat path.
179
+ *
180
+ * @param {Record<string, unknown>} payload
181
+ * @returns {boolean}
182
+ */
183
+ function isNotificationKind(payload) {
184
+ if (!payload || typeof payload !== 'object') return false;
185
+ const kind = payload.messageKind;
186
+ if (kind === undefined || kind === null) return true;
187
+ return kind === 'content';
188
+ }
189
+
190
+ /**
191
+ * Broadcast a parsed push payload to every controlled client. Failures
192
+ * on individual `postMessage` calls are swallowed — one offline tab
193
+ * shouldn't break delivery to the others. The whole broadcast is
194
+ * resolved (never rejected) so it can be safely passed to
195
+ * `event.waitUntil`.
196
+ *
197
+ * @param {ServiceWorkerGlobalScope} sw
198
+ * @param {string} eventName
199
+ * @param {Record<string, unknown>} payload
200
+ * @returns {Promise<void>}
201
+ */
202
+ async function dispatchPushToClients(sw, eventName, payload) {
203
+ try {
204
+ const clientList = await sw.clients.matchAll({
205
+ type: 'window',
206
+ includeUncontrolled: true
207
+ });
208
+ const envelope = {
209
+ type: REI_AMSG_POSTMESSAGE_TYPE,
210
+ event: eventName,
211
+ payload
212
+ };
213
+ for (const client of clientList) {
214
+ try {
215
+ client.postMessage(envelope);
216
+ } catch (_postError) {
217
+ // Per-client failures must not abort the broadcast.
218
+ }
219
+ }
220
+ } catch (_matchError) {
221
+ // No window clients available, or the matchAll call rejected.
222
+ // Either way, fail silently — notification rendering still wins.
223
+ }
224
+ }
225
+
92
226
  function readPushPayload(event) {
93
227
  if (!event.data) return null;
94
228
 
@@ -346,4 +480,4 @@ async function removeQueuedRequest(id) {
346
480
  });
347
481
  }
348
482
 
349
- export { REI_SW_MESSAGE_TYPE, installReiSW };
483
+ export { REI_AMSG_POSTMESSAGE_TYPE, REI_SW_EVENT, REI_SW_MESSAGE_TYPE, installReiSW };
package/dist/index.d.ts CHANGED
@@ -2,34 +2,79 @@
2
2
  * ReiStandard Service Worker helpers.
3
3
  *
4
4
  * Drop-in plugin for Service Workers that handles:
5
- * - Basic push payload -> notification rendering
6
- * - Offline request queueing and retry with Background Sync
5
+ * - Three-axis `push` payload dispatch keyed by `payload.messageKind`
6
+ * (see `@rei-standard/amsg-shared`). Every push is mirrored to every
7
+ * controlled client via `postMessage` under a per-kind event name.
8
+ * - Notification rendering for `messageKind: 'content'` (and legacy
9
+ * payloads without `messageKind`, for back-compat with 2.0.x
10
+ * producers).
11
+ * - Offline request queueing and retry with Background Sync.
7
12
  *
8
13
  * Notes:
9
14
  * - This plugin intentionally does not install `notificationclick`.
10
15
  * Main applications can implement their own click navigation logic.
16
+ * - `reasoning` / `tool_request` / `error` pushes are dispatched as
17
+ * `postMessage` events but **do not** trigger `showNotification` —
18
+ * apps render those in-app via the postMessage channel.
19
+ * - Blob envelopes (`{ _blob: true, key, url, messageKind? }`) are
20
+ * dispatched to clients verbatim. The SW never auto-fetches the
21
+ * blob body — that's the client's job.
11
22
  *
12
23
  * Usage (inside your sw.js):
13
- * import { installReiSW, REI_SW_MESSAGE_TYPE } from '@rei-standard/amsg-sw';
24
+ * import { installReiSW, REI_SW_EVENT, REI_SW_MESSAGE_TYPE } from '@rei-standard/amsg-sw';
14
25
  * installReiSW(self);
15
26
  *
16
27
  * Usage (inside your web app):
17
- * navigator.serviceWorker.controller?.postMessage({
18
- * type: REI_SW_MESSAGE_TYPE.ENQUEUE_REQUEST,
19
- * request: {
20
- * url: '/api/messages/send',
21
- * method: 'POST',
22
- * headers: { 'Content-Type': 'application/json' },
23
- * body: { text: 'hello' }
28
+ * navigator.serviceWorker.addEventListener('message', (e) => {
29
+ * if (e.data?.type !== 'REI_AMSG_PUSH') return;
30
+ * switch (e.data.event) {
31
+ * case REI_SW_EVENT.CONTENT_RECEIVED: // render in-app message
32
+ * case REI_SW_EVENT.REASONING_RECEIVED: // render thinking UI
33
+ * case REI_SW_EVENT.TOOL_REQUEST_RECEIVED: // prompt tool exec
34
+ * case REI_SW_EVENT.ERROR_RECEIVED: // show error toast
35
+ * case REI_SW_EVENT.UNKNOWN_RECEIVED: // legacy 2.0.x payload
24
36
  * }
25
37
  * });
26
38
  */
27
39
 
40
+ /**
41
+ * @typedef {import('@rei-standard/amsg-shared').AmsgPush} AmsgPush
42
+ * @typedef {import('@rei-standard/amsg-shared').ContentPush} ContentPush
43
+ * @typedef {import('@rei-standard/amsg-shared').ReasoningPush} ReasoningPush
44
+ * @typedef {import('@rei-standard/amsg-shared').ToolRequestPush} ToolRequestPush
45
+ * @typedef {import('@rei-standard/amsg-shared').ErrorPush} ErrorPush
46
+ */
47
+
28
48
  const REI_SW_DB_NAME = 'rei-sw';
29
49
  const REI_SW_DB_STORE = 'request-outbox';
30
50
  const REI_SW_DB_VERSION = 1;
31
51
  const REI_SW_SYNC_TAG = 'rei-sw-flush-request-outbox';
32
52
 
53
+ /**
54
+ * Wire-level message type for SW → client postMessage envelopes.
55
+ * Clients filter on `e.data.type === 'REI_AMSG_PUSH'` before reading
56
+ * `e.data.event` (which is one of {@link REI_SW_EVENT}'s values).
57
+ */
58
+ const REI_AMSG_POSTMESSAGE_TYPE = 'REI_AMSG_PUSH';
59
+
60
+ /**
61
+ * Per-kind event names dispatched to controlled clients. Each push the
62
+ * SW receives is mirrored to every window via
63
+ * `postMessage({ type: 'REI_AMSG_PUSH', event: <one of these>, payload })`.
64
+ *
65
+ * The mapping is keyed by `payload.messageKind`. Legacy payloads (and
66
+ * blob envelopes) without a `messageKind` field dispatch as
67
+ * {@link REI_SW_EVENT.UNKNOWN_RECEIVED} so apps can still handle 2.0.x
68
+ * producers during migration.
69
+ */
70
+ const REI_SW_EVENT = Object.freeze({
71
+ CONTENT_RECEIVED: 'rei-amsg-content-received',
72
+ REASONING_RECEIVED: 'rei-amsg-reasoning-received',
73
+ TOOL_REQUEST_RECEIVED: 'rei-amsg-tool-request-received',
74
+ ERROR_RECEIVED: 'rei-amsg-error-received',
75
+ UNKNOWN_RECEIVED: 'rei-amsg-unknown-received'
76
+ });
77
+
33
78
  const REI_SW_MESSAGE_TYPE = Object.freeze({
34
79
  ENQUEUE_REQUEST: 'REI_ENQUEUE_REQUEST',
35
80
  FLUSH_QUEUE: 'REI_FLUSH_QUEUE',
@@ -56,15 +101,25 @@ function installReiSW(sw, opts = {}) {
56
101
  const payload = readPushPayload(event);
57
102
  if (!payload) return;
58
103
 
59
- const notification = createNotificationFromPayload(payload, {
60
- defaultIcon,
61
- defaultBadge
62
- });
63
- if (!notification) return;
104
+ const eventName = resolveEventName(payload);
105
+ const shouldRenderNotification = isNotificationKind(payload);
106
+
107
+ /** @type {Array<Promise<unknown>>} */
108
+ const work = [dispatchPushToClients(sw, eventName, payload)];
109
+
110
+ if (shouldRenderNotification) {
111
+ const notification = createNotificationFromPayload(payload, {
112
+ defaultIcon,
113
+ defaultBadge
114
+ });
115
+ if (notification) {
116
+ work.push(
117
+ sw.registration.showNotification(notification.title, notification.options)
118
+ );
119
+ }
120
+ }
64
121
 
65
- event.waitUntil(
66
- sw.registration.showNotification(notification.title, notification.options)
67
- );
122
+ event.waitUntil(Promise.all(work));
68
123
  });
69
124
 
70
125
  sw.addEventListener('message', (event) => {
@@ -89,6 +144,85 @@ function installReiSW(sw, opts = {}) {
89
144
  });
90
145
  }
91
146
 
147
+ /**
148
+ * Map a parsed push payload to its corresponding per-kind event name.
149
+ * Falls back to `UNKNOWN_RECEIVED` for legacy 2.0.x payloads and blob
150
+ * envelopes without `messageKind`.
151
+ *
152
+ * @param {Record<string, unknown>} payload
153
+ * @returns {string}
154
+ */
155
+ function resolveEventName(payload) {
156
+ const kind = payload && typeof payload === 'object' ? payload.messageKind : undefined;
157
+ switch (kind) {
158
+ case 'content':
159
+ return REI_SW_EVENT.CONTENT_RECEIVED;
160
+ case 'reasoning':
161
+ return REI_SW_EVENT.REASONING_RECEIVED;
162
+ case 'tool_request':
163
+ return REI_SW_EVENT.TOOL_REQUEST_RECEIVED;
164
+ case 'error':
165
+ return REI_SW_EVENT.ERROR_RECEIVED;
166
+ default:
167
+ return REI_SW_EVENT.UNKNOWN_RECEIVED;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * True when the payload should trigger `showNotification`. Only
173
+ * `messageKind: 'content'` renders a notification; everything else
174
+ * (`reasoning`, `tool_request`, `error`) is dispatched silently so
175
+ * apps can render them in-app.
176
+ *
177
+ * Legacy payloads with no `messageKind` field still render a
178
+ * notification — that's the 2.0.x back-compat path.
179
+ *
180
+ * @param {Record<string, unknown>} payload
181
+ * @returns {boolean}
182
+ */
183
+ function isNotificationKind(payload) {
184
+ if (!payload || typeof payload !== 'object') return false;
185
+ const kind = payload.messageKind;
186
+ if (kind === undefined || kind === null) return true;
187
+ return kind === 'content';
188
+ }
189
+
190
+ /**
191
+ * Broadcast a parsed push payload to every controlled client. Failures
192
+ * on individual `postMessage` calls are swallowed — one offline tab
193
+ * shouldn't break delivery to the others. The whole broadcast is
194
+ * resolved (never rejected) so it can be safely passed to
195
+ * `event.waitUntil`.
196
+ *
197
+ * @param {ServiceWorkerGlobalScope} sw
198
+ * @param {string} eventName
199
+ * @param {Record<string, unknown>} payload
200
+ * @returns {Promise<void>}
201
+ */
202
+ async function dispatchPushToClients(sw, eventName, payload) {
203
+ try {
204
+ const clientList = await sw.clients.matchAll({
205
+ type: 'window',
206
+ includeUncontrolled: true
207
+ });
208
+ const envelope = {
209
+ type: REI_AMSG_POSTMESSAGE_TYPE,
210
+ event: eventName,
211
+ payload
212
+ };
213
+ for (const client of clientList) {
214
+ try {
215
+ client.postMessage(envelope);
216
+ } catch (_postError) {
217
+ // Per-client failures must not abort the broadcast.
218
+ }
219
+ }
220
+ } catch (_matchError) {
221
+ // No window clients available, or the matchAll call rejected.
222
+ // Either way, fail silently — notification rendering still wins.
223
+ }
224
+ }
225
+
92
226
  function readPushPayload(event) {
93
227
  if (!event.data) return null;
94
228
 
@@ -346,4 +480,4 @@ async function removeQueuedRequest(id) {
346
480
  });
347
481
  }
348
482
 
349
- export { REI_SW_MESSAGE_TYPE, installReiSW };
483
+ export { REI_AMSG_POSTMESSAGE_TYPE, REI_SW_EVENT, REI_SW_MESSAGE_TYPE, installReiSW };
package/dist/index.mjs CHANGED
@@ -3,6 +3,14 @@ var REI_SW_DB_NAME = "rei-sw";
3
3
  var REI_SW_DB_STORE = "request-outbox";
4
4
  var REI_SW_DB_VERSION = 1;
5
5
  var REI_SW_SYNC_TAG = "rei-sw-flush-request-outbox";
6
+ var REI_AMSG_POSTMESSAGE_TYPE = "REI_AMSG_PUSH";
7
+ var REI_SW_EVENT = Object.freeze({
8
+ CONTENT_RECEIVED: "rei-amsg-content-received",
9
+ REASONING_RECEIVED: "rei-amsg-reasoning-received",
10
+ TOOL_REQUEST_RECEIVED: "rei-amsg-tool-request-received",
11
+ ERROR_RECEIVED: "rei-amsg-error-received",
12
+ UNKNOWN_RECEIVED: "rei-amsg-unknown-received"
13
+ });
6
14
  var REI_SW_MESSAGE_TYPE = Object.freeze({
7
15
  ENQUEUE_REQUEST: "REI_ENQUEUE_REQUEST",
8
16
  FLUSH_QUEUE: "REI_FLUSH_QUEUE",
@@ -14,14 +22,21 @@ function installReiSW(sw, opts = {}) {
14
22
  sw.addEventListener("push", (event) => {
15
23
  const payload = readPushPayload(event);
16
24
  if (!payload) return;
17
- const notification = createNotificationFromPayload(payload, {
18
- defaultIcon,
19
- defaultBadge
20
- });
21
- if (!notification) return;
22
- event.waitUntil(
23
- sw.registration.showNotification(notification.title, notification.options)
24
- );
25
+ const eventName = resolveEventName(payload);
26
+ const shouldRenderNotification = isNotificationKind(payload);
27
+ const work = [dispatchPushToClients(sw, eventName, payload)];
28
+ if (shouldRenderNotification) {
29
+ const notification = createNotificationFromPayload(payload, {
30
+ defaultIcon,
31
+ defaultBadge
32
+ });
33
+ if (notification) {
34
+ work.push(
35
+ sw.registration.showNotification(notification.title, notification.options)
36
+ );
37
+ }
38
+ }
39
+ event.waitUntil(Promise.all(work));
25
40
  });
26
41
  sw.addEventListener("message", (event) => {
27
42
  const message = event.data;
@@ -41,6 +56,47 @@ function installReiSW(sw, opts = {}) {
41
56
  event.waitUntil(flushQueuedRequests(sw));
42
57
  });
43
58
  }
59
+ function resolveEventName(payload) {
60
+ const kind = payload && typeof payload === "object" ? payload.messageKind : void 0;
61
+ switch (kind) {
62
+ case "content":
63
+ return REI_SW_EVENT.CONTENT_RECEIVED;
64
+ case "reasoning":
65
+ return REI_SW_EVENT.REASONING_RECEIVED;
66
+ case "tool_request":
67
+ return REI_SW_EVENT.TOOL_REQUEST_RECEIVED;
68
+ case "error":
69
+ return REI_SW_EVENT.ERROR_RECEIVED;
70
+ default:
71
+ return REI_SW_EVENT.UNKNOWN_RECEIVED;
72
+ }
73
+ }
74
+ function isNotificationKind(payload) {
75
+ if (!payload || typeof payload !== "object") return false;
76
+ const kind = payload.messageKind;
77
+ if (kind === void 0 || kind === null) return true;
78
+ return kind === "content";
79
+ }
80
+ async function dispatchPushToClients(sw, eventName, payload) {
81
+ try {
82
+ const clientList = await sw.clients.matchAll({
83
+ type: "window",
84
+ includeUncontrolled: true
85
+ });
86
+ const envelope = {
87
+ type: REI_AMSG_POSTMESSAGE_TYPE,
88
+ event: eventName,
89
+ payload
90
+ };
91
+ for (const client of clientList) {
92
+ try {
93
+ client.postMessage(envelope);
94
+ } catch (_postError) {
95
+ }
96
+ }
97
+ } catch (_matchError) {
98
+ }
99
+ }
44
100
  function readPushPayload(event) {
45
101
  if (!event.data) return null;
46
102
  try {
@@ -243,6 +299,8 @@ async function removeQueuedRequest(id) {
243
299
  });
244
300
  }
245
301
  export {
302
+ REI_AMSG_POSTMESSAGE_TYPE,
303
+ REI_SW_EVENT,
246
304
  REI_SW_MESSAGE_TYPE,
247
305
  installReiSW
248
306
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rei-standard/amsg-sw",
3
- "version": "2.0.1",
4
- "description": "ReiStandard Active Messaging service worker SDK",
3
+ "version": "2.1.0-next.0",
4
+ "description": "ReiStandard Active Messaging service worker SDK — three-axis push schema (content / reasoning / tool_request / error) with per-kind client postMessage events",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/Tosd0/ReiStandard",
@@ -26,11 +26,15 @@
26
26
  "dist"
27
27
  ],
28
28
  "scripts": {
29
- "build": "tsup"
29
+ "build": "tsup",
30
+ "test": "node --test test/*.test.mjs"
30
31
  },
31
32
  "engines": {
32
33
  "node": ">=20"
33
34
  },
35
+ "dependencies": {
36
+ "@rei-standard/amsg-shared": "0.1.0-next.0"
37
+ },
34
38
  "devDependencies": {
35
39
  "tsup": "^8.0.0",
36
40
  "typescript": "^5.0.0"