@undefineds.co/xpod 0.3.66 → 0.3.68

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 (43) hide show
  1. package/config/components-ignore.json +1 -0
  2. package/dist/api/ApiServer.d.ts +9 -1
  3. package/dist/api/ApiServer.js +22 -1
  4. package/dist/api/ApiServer.js.map +1 -1
  5. package/dist/api/container/routes.js +13 -0
  6. package/dist/api/container/routes.js.map +1 -1
  7. package/dist/api/handlers/DeviceNotificationRuntime.d.ts +26 -0
  8. package/dist/api/handlers/DeviceNotificationRuntime.js +77 -0
  9. package/dist/api/handlers/DeviceNotificationRuntime.js.map +1 -0
  10. package/dist/api/handlers/DeviceNotificationTicketHandler.d.ts +50 -0
  11. package/dist/api/handlers/DeviceNotificationTicketHandler.js +130 -0
  12. package/dist/api/handlers/DeviceNotificationTicketHandler.js.map +1 -0
  13. package/dist/api/handlers/DeviceNotificationTicketHandler.jsonld +117 -0
  14. package/dist/api/runs/InngestRunExecutionBackend.d.ts +2 -2
  15. package/dist/api/tasks/InngestTaskScheduler.d.ts +4 -4
  16. package/dist/components/components.jsonld +6 -1
  17. package/dist/components/context.jsonld +122 -0
  18. package/dist/http/DeviceNotificationWebSocketServer.d.ts +29 -0
  19. package/dist/http/DeviceNotificationWebSocketServer.js +138 -0
  20. package/dist/http/DeviceNotificationWebSocketServer.js.map +1 -0
  21. package/dist/http/DeviceNotificationWebSocketServer.jsonld +148 -0
  22. package/dist/index.d.ts +7 -1
  23. package/dist/index.js +13 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/notifications/DeviceNotificationHub.d.ts +74 -0
  26. package/dist/notifications/DeviceNotificationHub.js +313 -0
  27. package/dist/notifications/DeviceNotificationHub.js.map +1 -0
  28. package/dist/notifications/DeviceNotificationHub.jsonld +258 -0
  29. package/dist/notifications/DeviceNotificationResourceListener.d.ts +21 -0
  30. package/dist/notifications/DeviceNotificationResourceListener.js +38 -0
  31. package/dist/notifications/DeviceNotificationResourceListener.js.map +1 -0
  32. package/dist/notifications/DeviceNotificationResourceListener.jsonld +98 -0
  33. package/dist/notifications/device-notification-protocol.d.ts +56 -0
  34. package/dist/notifications/device-notification-protocol.js +127 -0
  35. package/dist/notifications/device-notification-protocol.js.map +1 -0
  36. package/dist/notifications/device-notification-protocol.jsonld +29 -0
  37. package/dist/notifications/index.d.ts +3 -0
  38. package/dist/notifications/index.js +20 -0
  39. package/dist/notifications/index.js.map +1 -0
  40. package/dist/storage/ObservableResourceStore.d.ts +3 -0
  41. package/dist/storage/ObservableResourceStore.js +17 -1
  42. package/dist/storage/ObservableResourceStore.js.map +1 -1
  43. package/package.json +1 -1
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DeviceNotificationResourceListener = void 0;
4
+ class DeviceNotificationResourceListener {
5
+ constructor(options) {
6
+ this.origin = new URL(options.origin);
7
+ this.hub = options.hub;
8
+ }
9
+ async onResourceChanged(event) {
10
+ const object = this.toResourceUrl(event.path);
11
+ this.hub.publish({
12
+ topic: this.toParentTopic(object),
13
+ object,
14
+ operation: event.action,
15
+ });
16
+ }
17
+ toResourceUrl(path) {
18
+ try {
19
+ return new URL(path).toString();
20
+ }
21
+ catch {
22
+ return new URL(path, this.origin).toString();
23
+ }
24
+ }
25
+ toParentTopic(object) {
26
+ const url = new URL(object);
27
+ if (url.pathname.endsWith('/')) {
28
+ return url.toString();
29
+ }
30
+ const index = url.pathname.lastIndexOf('/');
31
+ url.pathname = index >= 0 ? url.pathname.slice(0, index + 1) : '/';
32
+ url.search = '';
33
+ url.hash = '';
34
+ return url.toString();
35
+ }
36
+ }
37
+ exports.DeviceNotificationResourceListener = DeviceNotificationResourceListener;
38
+ //# sourceMappingURL=DeviceNotificationResourceListener.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DeviceNotificationResourceListener.js","sourceRoot":"","sources":["../../src/notifications/DeviceNotificationResourceListener.ts"],"names":[],"mappings":";;;AAYA,MAAa,kCAAkC;IAI7C,YAAmB,OAAkD;QACnE,IAAI,CAAC,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACtC,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;IACzB,CAAC;IAEM,KAAK,CAAC,iBAAiB,CAAC,KAA0B;QACvD,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC9C,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;YACf,KAAK,EAAE,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC;YACjC,MAAM;YACN,SAAS,EAAE,KAAK,CAAC,MAAM;SACxB,CAAC,CAAC;IACL,CAAC;IAEO,aAAa,CAAC,IAAY;QAChC,IAAI,CAAC;YACH,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;QAClC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC/C,CAAC;IACH,CAAC;IAEO,aAAa,CAAC,MAAc;QAClC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;QAC5B,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/B,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;QACxB,CAAC;QACD,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QAC5C,GAAG,CAAC,QAAQ,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;QACnE,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC;QAChB,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC;QACd,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;IACxB,CAAC;CACF;AArCD,gFAqCC","sourcesContent":["import type { ResourceChangeEvent } from '../storage/ObservableResourceStore';\nimport type { DeviceNotificationOperation } from './device-notification-protocol';\n\nexport interface DeviceNotificationResourcePublisher {\n publish(event: { topic: string; object?: string; operation: DeviceNotificationOperation }): void;\n}\n\nexport interface DeviceNotificationResourceListenerOptions {\n origin: string;\n hub: DeviceNotificationResourcePublisher;\n}\n\nexport class DeviceNotificationResourceListener {\n private readonly origin: URL;\n private readonly hub: DeviceNotificationResourcePublisher;\n\n public constructor(options: DeviceNotificationResourceListenerOptions) {\n this.origin = new URL(options.origin);\n this.hub = options.hub;\n }\n\n public async onResourceChanged(event: ResourceChangeEvent): Promise<void> {\n const object = this.toResourceUrl(event.path);\n this.hub.publish({\n topic: this.toParentTopic(object),\n object,\n operation: event.action,\n });\n }\n\n private toResourceUrl(path: string): string {\n try {\n return new URL(path).toString();\n } catch {\n return new URL(path, this.origin).toString();\n }\n }\n\n private toParentTopic(object: string): string {\n const url = new URL(object);\n if (url.pathname.endsWith('/')) {\n return url.toString();\n }\n const index = url.pathname.lastIndexOf('/');\n url.pathname = index >= 0 ? url.pathname.slice(0, index + 1) : '/';\n url.search = '';\n url.hash = '';\n return url.toString();\n }\n}\n"]}
@@ -0,0 +1,98 @@
1
+ {
2
+ "@context": [
3
+ "https://linkedsoftwaredependencies.org/bundles/npm/@undefineds.co/xpod/^0.0.0/components/context.jsonld"
4
+ ],
5
+ "@id": "npmd:@undefineds.co/xpod",
6
+ "components": [
7
+ {
8
+ "@id": "undefineds:dist/notifications/DeviceNotificationResourceListener.jsonld#DeviceNotificationResourceListener",
9
+ "@type": "Class",
10
+ "requireElement": "DeviceNotificationResourceListener",
11
+ "parameters": [
12
+ {
13
+ "@id": "undefineds:dist/notifications/DeviceNotificationResourceListener.jsonld#DeviceNotificationResourceListener_options_origin",
14
+ "range": "xsd:string"
15
+ },
16
+ {
17
+ "@id": "undefineds:dist/notifications/DeviceNotificationResourceListener.jsonld#DeviceNotificationResourceListener_options_hub",
18
+ "range": "undefineds:dist/notifications/DeviceNotificationResourceListener.jsonld#DeviceNotificationResourcePublisher"
19
+ }
20
+ ],
21
+ "memberFields": [
22
+ {
23
+ "@id": "undefineds:dist/notifications/DeviceNotificationResourceListener.jsonld#DeviceNotificationResourceListener__member_origin",
24
+ "memberFieldName": "origin"
25
+ },
26
+ {
27
+ "@id": "undefineds:dist/notifications/DeviceNotificationResourceListener.jsonld#DeviceNotificationResourceListener__member_hub",
28
+ "memberFieldName": "hub"
29
+ },
30
+ {
31
+ "@id": "undefineds:dist/notifications/DeviceNotificationResourceListener.jsonld#DeviceNotificationResourceListener__member_constructor",
32
+ "memberFieldName": "constructor"
33
+ },
34
+ {
35
+ "@id": "undefineds:dist/notifications/DeviceNotificationResourceListener.jsonld#DeviceNotificationResourceListener__member_onResourceChanged",
36
+ "memberFieldName": "onResourceChanged"
37
+ },
38
+ {
39
+ "@id": "undefineds:dist/notifications/DeviceNotificationResourceListener.jsonld#DeviceNotificationResourceListener__member_toResourceUrl",
40
+ "memberFieldName": "toResourceUrl"
41
+ },
42
+ {
43
+ "@id": "undefineds:dist/notifications/DeviceNotificationResourceListener.jsonld#DeviceNotificationResourceListener__member_toParentTopic",
44
+ "memberFieldName": "toParentTopic"
45
+ }
46
+ ],
47
+ "constructorArguments": [
48
+ {
49
+ "@id": "undefineds:dist/notifications/DeviceNotificationResourceListener.jsonld#DeviceNotificationResourceListener_options__constructorArgument",
50
+ "fields": [
51
+ {
52
+ "keyRaw": "origin",
53
+ "value": {
54
+ "@id": "undefineds:dist/notifications/DeviceNotificationResourceListener.jsonld#DeviceNotificationResourceListener_options_origin"
55
+ }
56
+ },
57
+ {
58
+ "keyRaw": "hub",
59
+ "value": {
60
+ "@id": "undefineds:dist/notifications/DeviceNotificationResourceListener.jsonld#DeviceNotificationResourceListener_options_hub"
61
+ }
62
+ }
63
+ ]
64
+ }
65
+ ]
66
+ },
67
+ {
68
+ "@id": "undefineds:dist/notifications/DeviceNotificationResourceListener.jsonld#DeviceNotificationResourcePublisher",
69
+ "@type": "AbstractClass",
70
+ "requireElement": "DeviceNotificationResourcePublisher",
71
+ "parameters": [],
72
+ "memberFields": [
73
+ {
74
+ "@id": "undefineds:dist/notifications/DeviceNotificationResourceListener.jsonld#DeviceNotificationResourcePublisher__member_publish",
75
+ "memberFieldName": "publish"
76
+ }
77
+ ],
78
+ "constructorArguments": []
79
+ },
80
+ {
81
+ "@id": "undefineds:dist/notifications/DeviceNotificationResourceListener.jsonld#DeviceNotificationResourceListenerOptions",
82
+ "@type": "AbstractClass",
83
+ "requireElement": "DeviceNotificationResourceListenerOptions",
84
+ "parameters": [],
85
+ "memberFields": [
86
+ {
87
+ "@id": "undefineds:dist/notifications/DeviceNotificationResourceListener.jsonld#DeviceNotificationResourceListenerOptions__member_origin",
88
+ "memberFieldName": "origin"
89
+ },
90
+ {
91
+ "@id": "undefineds:dist/notifications/DeviceNotificationResourceListener.jsonld#DeviceNotificationResourceListenerOptions__member_hub",
92
+ "memberFieldName": "hub"
93
+ }
94
+ ],
95
+ "constructorArguments": []
96
+ }
97
+ ]
98
+ }
@@ -0,0 +1,56 @@
1
+ export declare const XPOD_NOTIFICATIONS_PROTOCOL = "xpod.notifications.v1";
2
+ export type DeviceNotificationOperation = 'create' | 'update' | 'delete' | 'invalidate';
3
+ export type DeviceNotificationClientFrame = {
4
+ type: 'hello';
5
+ protocol: typeof XPOD_NOTIFICATIONS_PROTOCOL;
6
+ resumeFrom?: number;
7
+ } | {
8
+ type: 'register';
9
+ requestId: string;
10
+ topics: string[];
11
+ } | {
12
+ type: 'unregister';
13
+ requestId: string;
14
+ topics: string[];
15
+ } | {
16
+ type: 'ack';
17
+ sequence: number;
18
+ };
19
+ export type DeviceNotificationServerFrame = {
20
+ type: 'ready';
21
+ connectionId: string;
22
+ sequence: number;
23
+ } | {
24
+ type: 'registered' | 'unregistered';
25
+ requestId: string;
26
+ topics: string[];
27
+ } | {
28
+ type: 'event';
29
+ sequence: number;
30
+ eventId: string;
31
+ topic: string;
32
+ object?: string;
33
+ operation: DeviceNotificationOperation;
34
+ emittedAt: string;
35
+ } | {
36
+ type: 'resync-required';
37
+ topics: string[];
38
+ reason: 'gap' | 'overflow' | 'expired';
39
+ } | {
40
+ type: 'error';
41
+ requestId?: string;
42
+ code: string;
43
+ message: string;
44
+ };
45
+ export interface ParseClientFrameOptions {
46
+ origin?: string;
47
+ seenRequestIds?: Set<string>;
48
+ maxTopicsPerBatch?: number;
49
+ }
50
+ export declare function parseClientFrame(input: unknown, options?: ParseClientFrameOptions): DeviceNotificationClientFrame;
51
+ export declare function serializeServerFrame(frame: DeviceNotificationServerFrame): string;
52
+ export declare function createProtocolErrorFrame(code: string, message: string, requestId?: string): DeviceNotificationServerFrame;
53
+ export declare function parseDeviceNotificationSubprotocols(header: string | string[] | undefined): {
54
+ protocolAccepted: boolean;
55
+ ticket?: string;
56
+ };
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.XPOD_NOTIFICATIONS_PROTOCOL = void 0;
4
+ exports.parseClientFrame = parseClientFrame;
5
+ exports.serializeServerFrame = serializeServerFrame;
6
+ exports.createProtocolErrorFrame = createProtocolErrorFrame;
7
+ exports.parseDeviceNotificationSubprotocols = parseDeviceNotificationSubprotocols;
8
+ exports.XPOD_NOTIFICATIONS_PROTOCOL = 'xpod.notifications.v1';
9
+ const CLIENT_FIELDS = {
10
+ hello: new Set(['type', 'protocol', 'resumeFrom']),
11
+ register: new Set(['type', 'requestId', 'topics']),
12
+ unregister: new Set(['type', 'requestId', 'topics']),
13
+ ack: new Set(['type', 'sequence']),
14
+ };
15
+ function parseClientFrame(input, options = {}) {
16
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
17
+ throw new Error('Invalid client frame');
18
+ }
19
+ const frame = input;
20
+ if (typeof frame.type !== 'string' || !(frame.type in CLIENT_FIELDS)) {
21
+ throw new Error(`Unknown frame type: ${String(frame.type)}`);
22
+ }
23
+ rejectUnknownFields(frame, CLIENT_FIELDS[frame.type]);
24
+ switch (frame.type) {
25
+ case 'hello':
26
+ return parseHello(frame);
27
+ case 'register':
28
+ case 'unregister':
29
+ return parseTopicFrame(frame, frame.type, options);
30
+ case 'ack':
31
+ return parseAck(frame);
32
+ default:
33
+ throw new Error(`Unknown frame type: ${String(frame.type)}`);
34
+ }
35
+ }
36
+ function serializeServerFrame(frame) {
37
+ return JSON.stringify(frame);
38
+ }
39
+ function createProtocolErrorFrame(code, message, requestId) {
40
+ return {
41
+ type: 'error',
42
+ ...(requestId ? { requestId } : {}),
43
+ code,
44
+ message: messageContainsSecret(message) ? '[redacted]' : message,
45
+ };
46
+ }
47
+ function parseDeviceNotificationSubprotocols(header) {
48
+ const raw = Array.isArray(header) ? header.join(',') : header ?? '';
49
+ const entries = raw.split(',').map((entry) => entry.trim()).filter(Boolean);
50
+ return {
51
+ protocolAccepted: entries.includes(exports.XPOD_NOTIFICATIONS_PROTOCOL),
52
+ ticket: entries.find((entry) => entry !== exports.XPOD_NOTIFICATIONS_PROTOCOL),
53
+ };
54
+ }
55
+ function parseHello(frame) {
56
+ if (frame.protocol !== exports.XPOD_NOTIFICATIONS_PROTOCOL) {
57
+ throw new Error('Unsupported notification protocol');
58
+ }
59
+ if (frame.resumeFrom !== undefined && !isNonNegativeInteger(frame.resumeFrom)) {
60
+ throw new Error('resumeFrom must be a non-negative integer');
61
+ }
62
+ return {
63
+ type: 'hello',
64
+ protocol: exports.XPOD_NOTIFICATIONS_PROTOCOL,
65
+ ...(frame.resumeFrom === undefined ? {} : { resumeFrom: frame.resumeFrom }),
66
+ };
67
+ }
68
+ function parseTopicFrame(frame, type, options) {
69
+ if (typeof frame.requestId !== 'string' || frame.requestId.length === 0) {
70
+ throw new Error('requestId is required');
71
+ }
72
+ if (options.seenRequestIds?.has(frame.requestId)) {
73
+ throw new Error(`Duplicate requestId: ${frame.requestId}`);
74
+ }
75
+ const topics = parseTopics(frame.topics, options);
76
+ options.seenRequestIds?.add(frame.requestId);
77
+ return { type, requestId: frame.requestId, topics };
78
+ }
79
+ function parseAck(frame) {
80
+ if (!isNonNegativeInteger(frame.sequence)) {
81
+ throw new Error('sequence must be a non-negative integer');
82
+ }
83
+ return { type: 'ack', sequence: frame.sequence };
84
+ }
85
+ function parseTopics(input, options) {
86
+ if (!Array.isArray(input) || input.length === 0) {
87
+ throw new Error('topics must be a non-empty array');
88
+ }
89
+ const maxTopics = options.maxTopicsPerBatch ?? 100;
90
+ if (input.length > maxTopics) {
91
+ throw new Error(`Too many topics: ${input.length}`);
92
+ }
93
+ return input.map((topic) => parseTopic(topic, options.origin));
94
+ }
95
+ function parseTopic(input, origin) {
96
+ if (typeof input !== 'string' || input.trim().length === 0) {
97
+ throw new Error('topic must be a URL string');
98
+ }
99
+ let url;
100
+ try {
101
+ url = new URL(input);
102
+ }
103
+ catch {
104
+ throw new Error('topic must be an absolute URL');
105
+ }
106
+ if (!['http:', 'https:'].includes(url.protocol)) {
107
+ throw new Error('topic must be an HTTP URL');
108
+ }
109
+ if (origin && url.origin !== new URL(origin).origin) {
110
+ throw new Error('topic must be a same-origin Pod resource URL');
111
+ }
112
+ return url.toString();
113
+ }
114
+ function rejectUnknownFields(frame, allowed) {
115
+ for (const key of Object.keys(frame)) {
116
+ if (!allowed.has(key)) {
117
+ throw new Error(`Unknown field: ${key}`);
118
+ }
119
+ }
120
+ }
121
+ function isNonNegativeInteger(value) {
122
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0;
123
+ }
124
+ function messageContainsSecret(message) {
125
+ return /token|ticket|secret|bearer/i.test(message);
126
+ }
127
+ //# sourceMappingURL=device-notification-protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-notification-protocol.js","sourceRoot":"","sources":["../../src/notifications/device-notification-protocol.ts"],"names":[],"mappings":"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","sourcesContent":["export const XPOD_NOTIFICATIONS_PROTOCOL = 'xpod.notifications.v1';\n\nexport type DeviceNotificationOperation = 'create' | 'update' | 'delete' | 'invalidate';\n\nexport type DeviceNotificationClientFrame =\n | { type: 'hello'; protocol: typeof XPOD_NOTIFICATIONS_PROTOCOL; resumeFrom?: number }\n | { type: 'register'; requestId: string; topics: string[] }\n | { type: 'unregister'; requestId: string; topics: string[] }\n | { type: 'ack'; sequence: number };\n\nexport type DeviceNotificationServerFrame =\n | { type: 'ready'; connectionId: string; sequence: number }\n | { type: 'registered' | 'unregistered'; requestId: string; topics: string[] }\n | {\n type: 'event';\n sequence: number;\n eventId: string;\n topic: string;\n object?: string;\n operation: DeviceNotificationOperation;\n emittedAt: string;\n }\n | { type: 'resync-required'; topics: string[]; reason: 'gap' | 'overflow' | 'expired' }\n | { type: 'error'; requestId?: string; code: string; message: string };\n\nexport interface ParseClientFrameOptions {\n origin?: string;\n seenRequestIds?: Set<string>;\n maxTopicsPerBatch?: number;\n}\n\nconst CLIENT_FIELDS: Record<string, Set<string>> = {\n hello: new Set(['type', 'protocol', 'resumeFrom']),\n register: new Set(['type', 'requestId', 'topics']),\n unregister: new Set(['type', 'requestId', 'topics']),\n ack: new Set(['type', 'sequence']),\n};\n\nexport function parseClientFrame(input: unknown, options: ParseClientFrameOptions = {}): DeviceNotificationClientFrame {\n if (!input || typeof input !== 'object' || Array.isArray(input)) {\n throw new Error('Invalid client frame');\n }\n const frame = input as Record<string, unknown>;\n if (typeof frame.type !== 'string' || !(frame.type in CLIENT_FIELDS)) {\n throw new Error(`Unknown frame type: ${String(frame.type)}`);\n }\n rejectUnknownFields(frame, CLIENT_FIELDS[frame.type]);\n\n switch (frame.type) {\n case 'hello':\n return parseHello(frame);\n case 'register':\n case 'unregister':\n return parseTopicFrame(frame, frame.type, options);\n case 'ack':\n return parseAck(frame);\n default:\n throw new Error(`Unknown frame type: ${String(frame.type)}`);\n }\n}\n\nexport function serializeServerFrame(frame: DeviceNotificationServerFrame): string {\n return JSON.stringify(frame);\n}\n\nexport function createProtocolErrorFrame(\n code: string,\n message: string,\n requestId?: string,\n): DeviceNotificationServerFrame {\n return {\n type: 'error',\n ...(requestId ? { requestId } : {}),\n code,\n message: messageContainsSecret(message) ? '[redacted]' : message,\n };\n}\n\nexport function parseDeviceNotificationSubprotocols(header: string | string[] | undefined): {\n protocolAccepted: boolean;\n ticket?: string;\n} {\n const raw = Array.isArray(header) ? header.join(',') : header ?? '';\n const entries = raw.split(',').map((entry) => entry.trim()).filter(Boolean);\n return {\n protocolAccepted: entries.includes(XPOD_NOTIFICATIONS_PROTOCOL),\n ticket: entries.find((entry) => entry !== XPOD_NOTIFICATIONS_PROTOCOL),\n };\n}\n\nfunction parseHello(frame: Record<string, unknown>): DeviceNotificationClientFrame {\n if (frame.protocol !== XPOD_NOTIFICATIONS_PROTOCOL) {\n throw new Error('Unsupported notification protocol');\n }\n if (frame.resumeFrom !== undefined && !isNonNegativeInteger(frame.resumeFrom)) {\n throw new Error('resumeFrom must be a non-negative integer');\n }\n return {\n type: 'hello',\n protocol: XPOD_NOTIFICATIONS_PROTOCOL,\n ...(frame.resumeFrom === undefined ? {} : { resumeFrom: frame.resumeFrom as number }),\n };\n}\n\nfunction parseTopicFrame(\n frame: Record<string, unknown>,\n type: 'register' | 'unregister',\n options: ParseClientFrameOptions,\n): DeviceNotificationClientFrame {\n if (typeof frame.requestId !== 'string' || frame.requestId.length === 0) {\n throw new Error('requestId is required');\n }\n if (options.seenRequestIds?.has(frame.requestId)) {\n throw new Error(`Duplicate requestId: ${frame.requestId}`);\n }\n const topics = parseTopics(frame.topics, options);\n options.seenRequestIds?.add(frame.requestId);\n return { type, requestId: frame.requestId, topics };\n}\n\nfunction parseAck(frame: Record<string, unknown>): DeviceNotificationClientFrame {\n if (!isNonNegativeInteger(frame.sequence)) {\n throw new Error('sequence must be a non-negative integer');\n }\n return { type: 'ack', sequence: frame.sequence as number };\n}\n\nfunction parseTopics(input: unknown, options: ParseClientFrameOptions): string[] {\n if (!Array.isArray(input) || input.length === 0) {\n throw new Error('topics must be a non-empty array');\n }\n const maxTopics = options.maxTopicsPerBatch ?? 100;\n if (input.length > maxTopics) {\n throw new Error(`Too many topics: ${input.length}`);\n }\n return input.map((topic) => parseTopic(topic, options.origin));\n}\n\nfunction parseTopic(input: unknown, origin?: string): string {\n if (typeof input !== 'string' || input.trim().length === 0) {\n throw new Error('topic must be a URL string');\n }\n let url: URL;\n try {\n url = new URL(input);\n } catch {\n throw new Error('topic must be an absolute URL');\n }\n if (!['http:', 'https:'].includes(url.protocol)) {\n throw new Error('topic must be an HTTP URL');\n }\n if (origin && url.origin !== new URL(origin).origin) {\n throw new Error('topic must be a same-origin Pod resource URL');\n }\n return url.toString();\n}\n\nfunction rejectUnknownFields(frame: Record<string, unknown>, allowed: Set<string>): void {\n for (const key of Object.keys(frame)) {\n if (!allowed.has(key)) {\n throw new Error(`Unknown field: ${key}`);\n }\n }\n}\n\nfunction isNonNegativeInteger(value: unknown): boolean {\n return typeof value === 'number' && Number.isInteger(value) && value >= 0;\n}\n\nfunction messageContainsSecret(message: string): boolean {\n return /token|ticket|secret|bearer/i.test(message);\n}\n"]}
@@ -0,0 +1,29 @@
1
+ {
2
+ "@context": [
3
+ "https://linkedsoftwaredependencies.org/bundles/npm/@undefineds.co/xpod/^0.0.0/components/context.jsonld"
4
+ ],
5
+ "@id": "npmd:@undefineds.co/xpod",
6
+ "components": [
7
+ {
8
+ "@id": "undefineds:dist/notifications/device-notification-protocol.jsonld#ParseClientFrameOptions",
9
+ "@type": "AbstractClass",
10
+ "requireElement": "ParseClientFrameOptions",
11
+ "parameters": [],
12
+ "memberFields": [
13
+ {
14
+ "@id": "undefineds:dist/notifications/device-notification-protocol.jsonld#ParseClientFrameOptions__member_origin",
15
+ "memberFieldName": "origin"
16
+ },
17
+ {
18
+ "@id": "undefineds:dist/notifications/device-notification-protocol.jsonld#ParseClientFrameOptions__member_seenRequestIds",
19
+ "memberFieldName": "seenRequestIds"
20
+ },
21
+ {
22
+ "@id": "undefineds:dist/notifications/device-notification-protocol.jsonld#ParseClientFrameOptions__member_maxTopicsPerBatch",
23
+ "memberFieldName": "maxTopicsPerBatch"
24
+ }
25
+ ],
26
+ "constructorArguments": []
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,3 @@
1
+ export * from './device-notification-protocol';
2
+ export * from './DeviceNotificationHub';
3
+ export * from './DeviceNotificationResourceListener';
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./device-notification-protocol"), exports);
18
+ __exportStar(require("./DeviceNotificationHub"), exports);
19
+ __exportStar(require("./DeviceNotificationResourceListener"), exports);
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/notifications/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,iEAA+C;AAC/C,0DAAwC;AACxC,uEAAqD","sourcesContent":["export * from './device-notification-protocol';\nexport * from './DeviceNotificationHub';\nexport * from './DeviceNotificationResourceListener';\n"]}
@@ -46,6 +46,7 @@ export interface ObservableResourceStoreOptions {
46
46
  * 3. 事件异步处理,不阻塞主流程
47
47
  */
48
48
  export declare class ObservableResourceStore<T extends ResourceStore = ResourceStore> extends PassthroughStore<T> {
49
+ private static readonly globalListeners;
49
50
  protected readonly logger: import("global-logger-factory").Logger<unknown>;
50
51
  private readonly emitter;
51
52
  private readonly listeners;
@@ -54,6 +55,8 @@ export declare class ObservableResourceStore<T extends ResourceStore = ResourceS
54
55
  * 添加监听器
55
56
  */
56
57
  addListener(listener: ResourceChangeListener): void;
58
+ static addGlobalListener(listener: ResourceChangeListener): void;
59
+ static removeGlobalListener(listener: ResourceChangeListener): void;
57
60
  /**
58
61
  * 移除监听器
59
62
  */
@@ -35,6 +35,17 @@ class ObservableResourceStore extends community_server_1.PassthroughStore {
35
35
  addListener(listener) {
36
36
  this.listeners.push(listener);
37
37
  }
38
+ static addGlobalListener(listener) {
39
+ if (!this.globalListeners.includes(listener)) {
40
+ this.globalListeners.push(listener);
41
+ }
42
+ }
43
+ static removeGlobalListener(listener) {
44
+ const index = this.globalListeners.indexOf(listener);
45
+ if (index !== -1) {
46
+ this.globalListeners.splice(index, 1);
47
+ }
48
+ }
38
49
  /**
39
50
  * 移除监听器
40
51
  */
@@ -102,7 +113,11 @@ class ObservableResourceStore extends community_server_1.PassthroughStore {
102
113
  * 通知所有监听器
103
114
  */
104
115
  notifyListeners(event) {
105
- for (const listener of this.listeners) {
116
+ const listeners = new Set([
117
+ ...ObservableResourceStore.globalListeners,
118
+ ...this.listeners,
119
+ ]);
120
+ for (const listener of listeners) {
106
121
  listener.onResourceChanged(event).catch((error) => {
107
122
  this.logger.error(`Listener error for ${event.path}: ${error}`);
108
123
  });
@@ -122,4 +137,5 @@ class ObservableResourceStore extends community_server_1.PassthroughStore {
122
137
  }
123
138
  }
124
139
  exports.ObservableResourceStore = ObservableResourceStore;
140
+ ObservableResourceStore.globalListeners = [];
125
141
  //# sourceMappingURL=ObservableResourceStore.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ObservableResourceStore.js","sourceRoot":"","sources":["../../src/storage/ObservableResourceStore.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ObservableResourceStore - 可观察的资源存储包装器\n *\n * 在资源变更时发出事件,供订阅者(如 UsageTracker、VectorIndexer)处理。\n * 采用事件驱动模式,解耦存储操作与副作用逻辑。\n */\n\nimport { EventEmitter } from 'node:events';\nimport { PassthroughStore } from '@solid/community-server';\nimport type {\n ChangeMap,\n Representation,\n RepresentationPreferences,\n ResourceIdentifier,\n Conditions,\n ResourceStore,\n} from '@solid/community-server';\nimport { getLoggerFor } from 'global-logger-factory';\n\n/**\n * 资源变更事件类型\n */\nexport type ResourceChangeAction = 'create' | 'update' | 'delete';\n\n/**\n * 资源变更事件\n */\nexport interface ResourceChangeEvent {\n /** 资源路径 */\n path: string;\n /** 变更类型 */\n action: ResourceChangeAction;\n /** 是否为容器 */\n isContainer: boolean;\n /** 变更时间戳 */\n timestamp: number;\n}\n\n/**\n * 资源变更监听器接口\n */\nexport interface ResourceChangeListener {\n /**\n * 处理资源变更事件\n * @param event 变更事件\n */\n onResourceChanged(event: ResourceChangeEvent): Promise<void>;\n}\n\nexport interface ObservableResourceStoreOptions {\n /** 资源变更监听器列表 */\n listeners?: ResourceChangeListener[];\n}\n\n/**\n * ObservableResourceStore - 在资源变更时发出事件\n *\n * 使用方式:\n * 1. 作为 Store 链的包装层\n * 2. 注册 listeners 处理变更事件\n * 3. 事件异步处理,不阻塞主流程\n */\nexport class ObservableResourceStore<T extends ResourceStore = ResourceStore> extends PassthroughStore<T> {\n protected readonly logger = getLoggerFor(this);\n private readonly emitter = new EventEmitter();\n private readonly listeners: ResourceChangeListener[];\n\n public constructor(source: T, options: ObservableResourceStoreOptions = {}) {\n super(source);\n this.listeners = options.listeners ?? [];\n\n // 注册内部事件处理\n this.emitter.on('resource:changed', (event: ResourceChangeEvent) => {\n this.notifyListeners(event);\n });\n }\n\n /**\n * 添加监听器\n */\n public addListener(listener: ResourceChangeListener): void {\n this.listeners.push(listener);\n }\n\n /**\n * 移除监听器\n */\n public removeListener(listener: ResourceChangeListener): void {\n const index = this.listeners.indexOf(listener);\n if (index !== -1) {\n this.listeners.splice(index, 1);\n }\n }\n\n /**\n * 添加资源(创建新资源)\n */\n public override async addResource(\n container: ResourceIdentifier,\n representation: Representation,\n conditions?: Conditions,\n ): Promise<ChangeMap> {\n const change = await super.addResource(container, representation, conditions);\n\n // 从 ChangeMap 中获取新创建的资源路径\n for (const [identifier] of change) {\n this.emitChange(identifier.path, 'create');\n }\n\n return change;\n }\n\n /**\n * 设置资源(创建或更新)\n */\n public override async setRepresentation(\n identifier: ResourceIdentifier,\n representation: Representation,\n conditions?: Conditions,\n ): Promise<ChangeMap> {\n // 检查资源是否已存在\n const exists = await this.resourceExists(identifier);\n const action: ResourceChangeAction = exists ? 'update' : 'create';\n\n const change = await super.setRepresentation(identifier, representation, conditions);\n\n this.emitChange(identifier.path, action);\n\n return change;\n }\n\n /**\n * 删除资源\n */\n public override async deleteResource(\n identifier: ResourceIdentifier,\n conditions?: Conditions,\n ): Promise<ChangeMap> {\n const change = await super.deleteResource(identifier, conditions);\n\n this.emitChange(identifier.path, 'delete');\n\n return change;\n }\n\n /**\n * 修改资源(PATCH)\n */\n public override async modifyResource(\n identifier: ResourceIdentifier,\n patch: Representation,\n conditions?: Conditions,\n ): Promise<ChangeMap> {\n const change = await super.modifyResource(identifier, patch, conditions);\n\n this.emitChange(identifier.path, 'update');\n\n return change;\n }\n\n /**\n * 发出资源变更事件\n */\n private emitChange(path: string, action: ResourceChangeAction): void {\n const event: ResourceChangeEvent = {\n path,\n action,\n isContainer: path.endsWith('/'),\n timestamp: Date.now(),\n };\n\n this.logger.debug(`Resource ${action}: ${path}`);\n\n // 异步发出事件,不阻塞主流程\n setImmediate(() => {\n this.emitter.emit('resource:changed', event);\n });\n }\n\n /**\n * 通知所有监听器\n */\n private notifyListeners(event: ResourceChangeEvent): void {\n for (const listener of this.listeners) {\n listener.onResourceChanged(event).catch((error) => {\n this.logger.error(`Listener error for ${event.path}: ${error}`);\n });\n }\n }\n\n /**\n * 检查资源是否存在\n */\n private async resourceExists(identifier: ResourceIdentifier): Promise<boolean> {\n try {\n await this.source.getRepresentation(identifier, {} as RepresentationPreferences);\n return true;\n } catch {\n return false;\n }\n }\n}\n"]}
1
+ {"version":3,"file":"ObservableResourceStore.js","sourceRoot":"","sources":["../../src/storage/ObservableResourceStore.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEH,6CAA2C;AAC3C,8DAA2D;AAS3D,iEAAqD;AAqCrD;;;;;;;GAOG;AACH,MAAa,uBAAiE,SAAQ,mCAAmB;IAOvG,YAAmB,MAAS,EAAE,UAA0C,EAAE;QACxE,KAAK,CAAC,MAAM,CAAC,CAAC;QALG,WAAM,GAAG,IAAA,oCAAY,EAAC,IAAI,CAAC,CAAC;QAC9B,YAAO,GAAG,IAAI,0BAAY,EAAE,CAAC;QAK5C,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC;QAEzC,WAAW;QACX,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,kBAAkB,EAAE,CAAC,KAA0B,EAAE,EAAE;YACjE,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACI,WAAW,CAAC,QAAgC;QACjD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChC,CAAC;IAEM,MAAM,CAAC,iBAAiB,CAAC,QAAgC;QAC9D,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC7C,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IAEM,MAAM,CAAC,oBAAoB,CAAC,QAAgC;QACjE,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACrD,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;YACjB,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;IAED;;OAEG;IACI,cAAc,CAAC,QAAgC;QACpD,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC/C,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;YACjB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAED;;OAEG;IACa,KAAK,CAAC,WAAW,CAC/B,SAA6B,EAC7B,cAA8B,EAC9B,UAAuB;QAEvB,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,SAAS,EAAE,cAAc,EAAE,UAAU,CAAC,CAAC;QAE9E,0BAA0B;QAC1B,KAAK,MAAM,CAAC,UAAU,CAAC,IAAI,MAAM,EAAE,CAAC;YAClC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAC7C,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;OAEG;IACa,KAAK,CAAC,iBAAiB,CACrC,UAA8B,EAC9B,cAA8B,EAC9B,UAAuB;QAEvB,YAAY;QACZ,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;QACrD,MAAM,MAAM,GAAyB,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;QAElE,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,iBAAiB,CAAC,UAAU,EAAE,cAAc,EAAE,UAAU,CAAC,CAAC;QAErF,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAEzC,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;OAEG;IACa,KAAK,CAAC,cAAc,CAClC,UAA8B,EAC9B,UAAuB;QAEvB,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,cAAc,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QAElE,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAE3C,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;OAEG;IACa,KAAK,CAAC,cAAc,CAClC,UAA8B,EAC9B,KAAqB,EACrB,UAAuB;QAEvB,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,cAAc,CAAC,UAAU,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC;QAEzE,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAE3C,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;OAEG;IACK,UAAU,CAAC,IAAY,EAAE,MAA4B;QAC3D,MAAM,KAAK,GAAwB;YACjC,IAAI;YACJ,MAAM;YACN,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;YAC/B,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;SACtB,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,MAAM,KAAK,IAAI,EAAE,CAAC,CAAC;QAEjD,gBAAgB;QAChB,YAAY,CAAC,GAAG,EAAE;YAChB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;QAC/C,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACK,eAAe,CAAC,KAA0B;QAChD,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;YACxB,GAAG,uBAAuB,CAAC,eAAe;YAC1C,GAAG,IAAI,CAAC,SAAS;SAClB,CAAC,CAAC;QACH,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;YACjC,QAAQ,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;gBAChD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,sBAAsB,KAAK,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC;YAClE,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,cAAc,CAAC,UAA8B;QACzD,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,UAAU,EAAE,EAA+B,CAAC,CAAC;YACjF,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;;AA7JH,0DA8JC;AA7JyB,uCAAe,GAA6B,EAAE,AAA/B,CAAgC","sourcesContent":["/**\n * ObservableResourceStore - 可观察的资源存储包装器\n *\n * 在资源变更时发出事件,供订阅者(如 UsageTracker、VectorIndexer)处理。\n * 采用事件驱动模式,解耦存储操作与副作用逻辑。\n */\n\nimport { EventEmitter } from 'node:events';\nimport { PassthroughStore } from '@solid/community-server';\nimport type {\n ChangeMap,\n Representation,\n RepresentationPreferences,\n ResourceIdentifier,\n Conditions,\n ResourceStore,\n} from '@solid/community-server';\nimport { getLoggerFor } from 'global-logger-factory';\n\n/**\n * 资源变更事件类型\n */\nexport type ResourceChangeAction = 'create' | 'update' | 'delete';\n\n/**\n * 资源变更事件\n */\nexport interface ResourceChangeEvent {\n /** 资源路径 */\n path: string;\n /** 变更类型 */\n action: ResourceChangeAction;\n /** 是否为容器 */\n isContainer: boolean;\n /** 变更时间戳 */\n timestamp: number;\n}\n\n/**\n * 资源变更监听器接口\n */\nexport interface ResourceChangeListener {\n /**\n * 处理资源变更事件\n * @param event 变更事件\n */\n onResourceChanged(event: ResourceChangeEvent): Promise<void>;\n}\n\nexport interface ObservableResourceStoreOptions {\n /** 资源变更监听器列表 */\n listeners?: ResourceChangeListener[];\n}\n\n/**\n * ObservableResourceStore - 在资源变更时发出事件\n *\n * 使用方式:\n * 1. 作为 Store 链的包装层\n * 2. 注册 listeners 处理变更事件\n * 3. 事件异步处理,不阻塞主流程\n */\nexport class ObservableResourceStore<T extends ResourceStore = ResourceStore> extends PassthroughStore<T> {\n private static readonly globalListeners: ResourceChangeListener[] = [];\n\n protected readonly logger = getLoggerFor(this);\n private readonly emitter = new EventEmitter();\n private readonly listeners: ResourceChangeListener[];\n\n public constructor(source: T, options: ObservableResourceStoreOptions = {}) {\n super(source);\n this.listeners = options.listeners ?? [];\n\n // 注册内部事件处理\n this.emitter.on('resource:changed', (event: ResourceChangeEvent) => {\n this.notifyListeners(event);\n });\n }\n\n /**\n * 添加监听器\n */\n public addListener(listener: ResourceChangeListener): void {\n this.listeners.push(listener);\n }\n\n public static addGlobalListener(listener: ResourceChangeListener): void {\n if (!this.globalListeners.includes(listener)) {\n this.globalListeners.push(listener);\n }\n }\n\n public static removeGlobalListener(listener: ResourceChangeListener): void {\n const index = this.globalListeners.indexOf(listener);\n if (index !== -1) {\n this.globalListeners.splice(index, 1);\n }\n }\n\n /**\n * 移除监听器\n */\n public removeListener(listener: ResourceChangeListener): void {\n const index = this.listeners.indexOf(listener);\n if (index !== -1) {\n this.listeners.splice(index, 1);\n }\n }\n\n /**\n * 添加资源(创建新资源)\n */\n public override async addResource(\n container: ResourceIdentifier,\n representation: Representation,\n conditions?: Conditions,\n ): Promise<ChangeMap> {\n const change = await super.addResource(container, representation, conditions);\n\n // 从 ChangeMap 中获取新创建的资源路径\n for (const [identifier] of change) {\n this.emitChange(identifier.path, 'create');\n }\n\n return change;\n }\n\n /**\n * 设置资源(创建或更新)\n */\n public override async setRepresentation(\n identifier: ResourceIdentifier,\n representation: Representation,\n conditions?: Conditions,\n ): Promise<ChangeMap> {\n // 检查资源是否已存在\n const exists = await this.resourceExists(identifier);\n const action: ResourceChangeAction = exists ? 'update' : 'create';\n\n const change = await super.setRepresentation(identifier, representation, conditions);\n\n this.emitChange(identifier.path, action);\n\n return change;\n }\n\n /**\n * 删除资源\n */\n public override async deleteResource(\n identifier: ResourceIdentifier,\n conditions?: Conditions,\n ): Promise<ChangeMap> {\n const change = await super.deleteResource(identifier, conditions);\n\n this.emitChange(identifier.path, 'delete');\n\n return change;\n }\n\n /**\n * 修改资源(PATCH)\n */\n public override async modifyResource(\n identifier: ResourceIdentifier,\n patch: Representation,\n conditions?: Conditions,\n ): Promise<ChangeMap> {\n const change = await super.modifyResource(identifier, patch, conditions);\n\n this.emitChange(identifier.path, 'update');\n\n return change;\n }\n\n /**\n * 发出资源变更事件\n */\n private emitChange(path: string, action: ResourceChangeAction): void {\n const event: ResourceChangeEvent = {\n path,\n action,\n isContainer: path.endsWith('/'),\n timestamp: Date.now(),\n };\n\n this.logger.debug(`Resource ${action}: ${path}`);\n\n // 异步发出事件,不阻塞主流程\n setImmediate(() => {\n this.emitter.emit('resource:changed', event);\n });\n }\n\n /**\n * 通知所有监听器\n */\n private notifyListeners(event: ResourceChangeEvent): void {\n const listeners = new Set([\n ...ObservableResourceStore.globalListeners,\n ...this.listeners,\n ]);\n for (const listener of listeners) {\n listener.onResourceChanged(event).catch((error) => {\n this.logger.error(`Listener error for ${event.path}: ${error}`);\n });\n }\n }\n\n /**\n * 检查资源是否存在\n */\n private async resourceExists(identifier: ResourceIdentifier): Promise<boolean> {\n try {\n await this.source.getRepresentation(identifier, {} as RepresentationPreferences);\n return true;\n } catch {\n return false;\n }\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@undefineds.co/xpod",
3
- "version": "0.3.66",
3
+ "version": "0.3.68",
4
4
  "description": "Xpod is an extended Community Solid Server, offering rich-feature, production-level Solid Pod and identity management.",
5
5
  "repository": "https://github.com/undefinedsco/xpod",
6
6
  "author": "developer@undefineds.co",