@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 +55 -1
- package/dist/index.cjs +66 -8
- package/dist/index.d.cts +153 -19
- package/dist/index.d.ts +153 -19
- package/dist/index.mjs +66 -8
- package/package.json +7 -3
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`
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
* -
|
|
6
|
-
*
|
|
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.
|
|
18
|
-
* type
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
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
|
-
* -
|
|
6
|
-
*
|
|
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.
|
|
18
|
-
* type
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
|
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"
|