sonamu 0.9.5 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/config.d.ts +13 -2
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +1 -1
- package/dist/api/context.d.ts +17 -7
- package/dist/api/context.d.ts.map +1 -1
- package/dist/api/context.js +1 -1
- package/dist/api/decorators.d.ts +18 -0
- package/dist/api/decorators.d.ts.map +1 -1
- package/dist/api/decorators.js +55 -4
- package/dist/api/index.js +8 -3
- package/dist/api/sonamu.d.ts +24 -9
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +365 -79
- package/dist/api/websocket-helpers.d.ts +24 -0
- package/dist/api/websocket-helpers.d.ts.map +1 -0
- package/dist/api/websocket-helpers.js +77 -0
- package/dist/bin/cli.js +12 -4
- package/dist/dict/sonamu-dictionary.js +5 -5
- package/dist/entity/entity-manager.js +1 -1
- package/dist/entity/entity.js +3 -3
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -4
- package/dist/migration/code-generation.js +7 -7
- package/dist/stream/index.d.ts +6 -0
- package/dist/stream/index.d.ts.map +1 -1
- package/dist/stream/index.js +13 -2
- package/dist/stream/ws-audience-resolver.d.ts +15 -0
- package/dist/stream/ws-audience-resolver.d.ts.map +1 -0
- package/dist/stream/ws-audience-resolver.js +31 -0
- package/dist/stream/ws-audience.d.ts +28 -0
- package/dist/stream/ws-audience.d.ts.map +1 -0
- package/dist/stream/ws-audience.js +46 -0
- package/dist/stream/ws-cluster-bus.d.ts +23 -0
- package/dist/stream/ws-cluster-bus.d.ts.map +1 -0
- package/dist/stream/ws-cluster-bus.js +18 -0
- package/dist/stream/ws-core.d.ts +15 -0
- package/dist/stream/ws-core.d.ts.map +1 -0
- package/dist/stream/ws-core.js +1 -0
- package/dist/stream/ws-delivery.d.ts +24 -0
- package/dist/stream/ws-delivery.d.ts.map +1 -0
- package/dist/stream/ws-delivery.js +103 -0
- package/dist/stream/ws-local-connection-store.d.ts +10 -0
- package/dist/stream/ws-local-connection-store.d.ts.map +1 -0
- package/dist/stream/ws-local-connection-store.js +44 -0
- package/dist/stream/ws-presence-store.d.ts +61 -0
- package/dist/stream/ws-presence-store.d.ts.map +1 -0
- package/dist/stream/ws-presence-store.js +236 -0
- package/dist/stream/ws-registry.d.ts +42 -0
- package/dist/stream/ws-registry.d.ts.map +1 -0
- package/dist/stream/ws-registry.js +108 -0
- package/dist/stream/ws.d.ts +52 -0
- package/dist/stream/ws.d.ts.map +1 -0
- package/dist/stream/ws.js +397 -0
- package/dist/syncer/api-parser.d.ts.map +1 -1
- package/dist/syncer/api-parser.js +72 -2
- package/dist/syncer/checksum.d.ts.map +1 -1
- package/dist/syncer/checksum.js +13 -12
- package/dist/syncer/code-generator.d.ts.map +1 -1
- package/dist/syncer/code-generator.js +7 -4
- package/dist/syncer/event-batcher.d.ts +27 -0
- package/dist/syncer/event-batcher.d.ts.map +1 -0
- package/dist/syncer/event-batcher.js +69 -0
- package/dist/syncer/file-patterns.d.ts +48 -26
- package/dist/syncer/file-patterns.d.ts.map +1 -1
- package/dist/syncer/file-patterns.js +71 -23
- package/dist/syncer/file-tracking.d.ts +13 -0
- package/dist/syncer/file-tracking.d.ts.map +1 -0
- package/dist/syncer/file-tracking.js +33 -0
- package/dist/syncer/index.js +2 -2
- package/dist/syncer/module-loader.d.ts +2 -11
- package/dist/syncer/module-loader.d.ts.map +1 -1
- package/dist/syncer/module-loader.js +3 -3
- package/dist/syncer/syncer-actions.d.ts +39 -6
- package/dist/syncer/syncer-actions.d.ts.map +1 -1
- package/dist/syncer/syncer-actions.js +125 -10
- package/dist/syncer/syncer.d.ts +33 -19
- package/dist/syncer/syncer.d.ts.map +1 -1
- package/dist/syncer/syncer.js +168 -168
- package/dist/syncer/watcher.d.ts +8 -0
- package/dist/syncer/watcher.d.ts.map +1 -0
- package/dist/syncer/watcher.js +105 -0
- package/dist/tasks/workflow-manager.d.ts.map +1 -1
- package/dist/tasks/workflow-manager.js +2 -1
- package/dist/template/implementations/services.template.d.ts.map +1 -1
- package/dist/template/implementations/services.template.js +36 -1
- package/dist/testing/bootstrap.d.ts.map +1 -1
- package/dist/testing/bootstrap.js +8 -1
- package/dist/testing/fixture-manager.js +1 -1
- package/dist/types/types.d.ts +2 -1
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/types.js +2 -2
- package/dist/ui/api.js +1 -1
- package/dist/ui/cdd-service.js +1 -1
- package/dist/ui-web/assets/{index-DzZ7vBk4.js → index-BmThfg-s.js} +37 -37
- package/dist/ui-web/index.html +1 -1
- package/dist/utils/async-utils.d.ts +27 -3
- package/dist/utils/async-utils.d.ts.map +1 -1
- package/dist/utils/async-utils.js +56 -6
- package/dist/utils/formatter.d.ts +7 -1
- package/dist/utils/formatter.d.ts.map +1 -1
- package/dist/utils/formatter.js +93 -59
- package/dist/utils/fs-utils.d.ts +2 -0
- package/dist/utils/fs-utils.d.ts.map +1 -1
- package/dist/utils/fs-utils.js +10 -2
- package/dist/utils/process-utils.d.ts +10 -4
- package/dist/utils/process-utils.d.ts.map +1 -1
- package/dist/utils/process-utils.js +20 -7
- package/dist/utils/utils.d.ts +1 -0
- package/dist/utils/utils.d.ts.map +1 -1
- package/dist/utils/utils.js +2 -2
- package/package.json +3 -1
- package/src/api/__tests__/sonamu.websocket.test.ts +64 -0
- package/src/api/__tests__/websocket-context.types.test.ts +58 -0
- package/src/api/config.ts +28 -2
- package/src/api/context.ts +21 -7
- package/src/api/decorators.ts +103 -3
- package/src/api/sonamu.ts +529 -127
- package/src/api/websocket-helpers.ts +122 -0
- package/src/bin/cli.ts +10 -2
- package/src/dict/sonamu-dictionary.ts +2 -2
- package/src/entity/entity.ts +1 -1
- package/src/index.ts +6 -0
- package/src/migration/code-generation.ts +5 -5
- package/src/shared/app.shared.ts.txt +254 -1
- package/src/shared/web.shared.ts.txt +282 -1
- package/src/stream/__tests__/ws-contracts.test.ts +381 -0
- package/src/stream/__tests__/ws.test.ts +449 -0
- package/src/stream/index.ts +6 -0
- package/src/stream/ws-audience-resolver.ts +35 -0
- package/src/stream/ws-audience.ts +62 -0
- package/src/stream/ws-cluster-bus.ts +32 -0
- package/src/stream/ws-core.ts +16 -0
- package/src/stream/ws-delivery.ts +138 -0
- package/src/stream/ws-local-connection-store.ts +44 -0
- package/src/stream/ws-presence-store.ts +326 -0
- package/src/stream/ws-registry.ts +138 -0
- package/src/stream/ws.ts +591 -0
- package/src/syncer/__tests__/api-parser.websocket-type-ref.test.ts +78 -0
- package/src/syncer/api-parser.ts +112 -1
- package/src/syncer/checksum.ts +23 -29
- package/src/syncer/code-generator.ts +4 -1
- package/src/syncer/event-batcher.ts +72 -0
- package/src/syncer/file-patterns.ts +98 -30
- package/src/syncer/file-tracking.ts +27 -0
- package/src/syncer/module-loader.ts +5 -12
- package/src/syncer/syncer-actions.ts +179 -17
- package/src/syncer/syncer.ts +250 -287
- package/src/syncer/watcher.ts +128 -0
- package/src/tasks/workflow-manager.ts +1 -0
- package/src/template/__tests__/services.template.websocket.test.ts +79 -0
- package/src/template/implementations/services.template.ts +69 -0
- package/src/testing/bootstrap.ts +8 -1
- package/src/types/types.ts +20 -2
- package/src/utils/async-utils.ts +71 -4
- package/src/utils/formatter.ts +111 -74
- package/src/utils/fs-utils.ts +9 -0
- package/src/utils/process-utils.ts +21 -4
- package/src/utils/utils.ts +1 -1
package/dist/utils/utils.js
CHANGED
|
@@ -92,5 +92,5 @@ var init_utils = __esmMin((() => {}));
|
|
|
92
92
|
|
|
93
93
|
//#endregion
|
|
94
94
|
init_utils();
|
|
95
|
-
export { assertDefined, assertExists, assertNotNull, convertFastifyHeadersToStandard, differenceWith, exhaustive, findApiRootPath, findAppRootPath, init_utils, intersectionBy, merge, nonNullable };
|
|
96
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbHMuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL3V0aWxzL3V0aWxzLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBmcyBmcm9tIFwiZnNcIjtcbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XG5cbmltcG9ydCB7IHR5cGUgRmFzdGlmeVJlcXVlc3QgfSBmcm9tIFwiZmFzdGlmeVwiO1xuXG5pbXBvcnQgeyB0eXBlIEFic29sdXRlUGF0aCB9IGZyb20gXCIuL3BhdGgtdXRpbHNcIjtcblxuZXhwb3J0IGZ1bmN0aW9uIGZpbmRBcHBSb290UGF0aCgpOiBBYnNvbHV0ZVBhdGgge1xuICBjb25zdCBhcGlSb290UGF0aCA9IGZpbmRBcGlSb290UGF0aCgpO1xuICByZXR1cm4gcGF0aC5kaXJuYW1lKGFwaVJvb3RQYXRoKSBhcyBBYnNvbHV0ZVBhdGg7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBmaW5kQXBpUm9vdFBhdGgoKTogQWJzb2x1dGVQYXRoIHtcbiAgLy8gTk9URTogZm9yIHN1cHBvcnQgbnBtIC8geWFybiAvIHBucG0gd29ya3NwYWNlc1xuICAvLyDtlZjsp4Drp4wgd29ya3NwYWNlIOyTsOuptCBwcm9jZXNzLmN3ZCgpIO2VmOuptCDrkJjripTrjbAuLi4g7J206rG0IOuCmOykkeyXkCDtmJHsnZgg7ZuEIOyImOygle2VmOuKlOqxuOuhnFxuICBjb25zdCB3b3Jrc3BhY2VQYXRoID0gcHJvY2Vzcy5lbnYuUE5QTV9TQ1JJUFRfU1JDX0RJUiA/PyBwcm9jZXNzLmVudi5JTklUX0NXRDtcbiAgaWYgKG5vbk51bGxhYmxlKHdvcmtzcGFjZVBhdGgpKSB7XG4gICAgcmV0dXJuIHdvcmtzcGFjZVBhdGggYXMgQWJzb2x1dGVQYXRoO1xuICB9XG5cbiAgaWYgKG5vbk51bGxhYmxlKHByb2Nlc3MuZW52LlBOUE1fUEFDS0FHRV9OQU1FKSkge1xuICAgIHJldHVybiBwcm9jZXNzLmN3ZCgpLnNwbGl0KHBhdGguc2VwKS5qb2luKHBhdGguc2VwKSBhcyBBYnNvbHV0ZVBhdGg7XG4gIH1cblxuICBjb25zdCBjd2RQYWNrYWdlUGF0aCA9IHBhdGguam9pbihwcm9jZXNzLmN3ZCgpLCBcInBhY2thZ2UuanNvblwiKTtcbiAgaWYgKGZzLmV4aXN0c1N5bmMoY3dkUGFja2FnZVBhdGgpKSB7XG4gICAgcmV0dXJuIHByb2Nlc3MuY3dkKCkuc3BsaXQocGF0aC5zZXApLmpvaW4ocGF0aC5zZXApIGFzIEFic29sdXRlUGF0aDtcbiAgfVxuXG4gIGNvbnN0IGJhc2VQYXRoID0gaW1wb3J0Lm1ldGEuZmlsZW5hbWU7XG4gIGxldCBkaXIgPSBwYXRoLmRpcm5hbWUoYmFzZVBhdGgpO1xuICBpZiAoZGlyLmluY2x1ZGVzKFwiLy55YXJuL1wiKSkge1xuICAgIGRpciA9IGRpci5zcGxpdChcIi8ueWFybi9cIilbMF07XG4gIH1cblxuICBkbyB7XG4gICAgaWYgKGZzLmV4aXN0c1N5bmMocGF0aC5qb2luKGRpciwgXCIvcGFja2FnZS5qc29uXCIpKSkge1xuICAgICAgcmV0dXJuIGRpci5zcGxpdChwYXRoLnNlcCkuam9pbihwYXRoLnNlcCkgYXMgQWJzb2x1dGVQYXRoO1xuICAgIH1cbiAgICBkaXIgPSBkaXIuc3BsaXQocGF0aC5zZXApLnNsaWNlKDAsIC0xKS5qb2luKHBhdGguc2VwKTtcbiAgfSB3aGlsZSAoZGlyLnNwbGl0KHBhdGguc2VwKS5sZW5ndGggPiAxKTtcbiAgdGhyb3cgbmV3IEVycm9yKFwiQ2Fubm90IGZpbmQgQXBwUm9vdCB1c2luZyBTb25hbXUgLTJcIik7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBub25OdWxsYWJsZTxUPih2YWx1ZTogVCk6IHZhbHVlIGlzIE5vbk51bGxhYmxlPFQ+IHtcbiAgcmV0dXJuIHZhbHVlICE9PSBudWxsICYmIHZhbHVlICE9PSB1bmRlZmluZWQ7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBleGhhdXN0aXZlKF9wYXJhbTogbmV2ZXIpIHtcbiAgdGhyb3cgbmV3IEVycm9yKGBleGhhdXN0aXZlYCk7XG59XG5cbi8vIOydvOuwmCDrsoTsoIRcbmV4cG9ydCBmdW5jdGlvbiBhc3NlcnRFeGlzdHM8VD4odmFsdWU6IFQgfCBudWxsIHwgdW5kZWZpbmVkLCBtZXNzYWdlPzogc3RyaW5nKTogVCB7XG4gIGlmICh2YWx1ZSA9PT0gbnVsbCB8fCB2YWx1ZSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKG1lc3NhZ2UgPz8gXCJWYWx1ZSBtdXN0IGV4aXN0XCIpO1xuICB9XG4gIHJldHVybiB2YWx1ZTtcbn1cblxuLy8gbnVsbOunjCDssrTtgaxcbmV4cG9ydCBmdW5jdGlvbiBhc3NlcnROb3ROdWxsPFQ+KHZhbHVlOiBUIHwgbnVsbCwgbWVzc2FnZT86IHN0cmluZyk6IFQge1xuICBpZiAodmFsdWUgPT09IG51bGwpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IobWVzc2FnZSA/PyBcIlZhbHVlIG11c3Qgbm90IGJlIG51bGxcIik7XG4gIH1cbiAgcmV0dXJuIHZhbHVlO1xufVxuLy8gdW5kZWZpbmVk66eMIOyytO2BrFxuZXhwb3J0IGZ1bmN0aW9uIGFzc2VydERlZmluZWQ8VD4odmFsdWU6IFQgfCB1bmRlZmluZWQsIG1lc3NhZ2U/OiBzdHJpbmcpOiBUIHtcbiAgaWYgKHZhbHVlID09PSB1bmRlZmluZWQpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IobWVzc2FnZSA/PyBcIlZhbHVlIG11c3QgYmUgZGVmaW5lZFwiKTtcbiAgfVxuICByZXR1cm4gdmFsdWU7XG59XG5cbi8vIGxvZGFzaCBpbnRlcnNlY3Rpb25CeSDrjIDssrRcbmV4cG9ydCBmdW5jdGlvbiBpbnRlcnNlY3Rpb25CeTxULCBLPihcbiAgYXJyMTogcmVhZG9ubHkgVFtdLFxuICBhcnIyOiByZWFkb25seSBUW10sXG4gIGl0ZXJhdGVlOiAoaXRlbTogVCkgPT4gSyxcbik6IFRbXSB7XG4gIGNvbnN0IGFycjJLZXlzID0gbmV3IFNldChhcnIyLm1hcChpdGVyYXRlZSkpO1xuICByZXR1cm4gYXJyMS5maWx0ZXIoKGl0ZW0pID0+IGFycjJLZXlzLmhhcyhpdGVyYXRlZShpdGVtKSkpO1xufVxuLy8gbG9kYXNoIGRpZmZlcmVuY2VXaXRoIOuMgOyytFxuZXhwb3J0IGZ1bmN0aW9uIGRpZmZlcmVuY2VXaXRoPFQ+KFxuICBhcnIxOiByZWFkb25seSBUW10sXG4gIGFycjI6IHJlYWRvbmx5IFRbXSxcbiAgY29tcGFyYXRvcjogKGE6IFQsIGI6IFQpID0+
|
|
95
|
+
export { assertDefined, assertExists, assertNotNull, convertFastifyHeadersToStandard, differenceWith, exhaustive, findApiRootPath, findAppRootPath, init_utils, intersectionBy, isPlainObject, merge, nonNullable };
|
|
96
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbHMuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL3V0aWxzL3V0aWxzLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBmcyBmcm9tIFwiZnNcIjtcbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XG5cbmltcG9ydCB7IHR5cGUgRmFzdGlmeVJlcXVlc3QgfSBmcm9tIFwiZmFzdGlmeVwiO1xuXG5pbXBvcnQgeyB0eXBlIEFic29sdXRlUGF0aCB9IGZyb20gXCIuL3BhdGgtdXRpbHNcIjtcblxuZXhwb3J0IGZ1bmN0aW9uIGZpbmRBcHBSb290UGF0aCgpOiBBYnNvbHV0ZVBhdGgge1xuICBjb25zdCBhcGlSb290UGF0aCA9IGZpbmRBcGlSb290UGF0aCgpO1xuICByZXR1cm4gcGF0aC5kaXJuYW1lKGFwaVJvb3RQYXRoKSBhcyBBYnNvbHV0ZVBhdGg7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBmaW5kQXBpUm9vdFBhdGgoKTogQWJzb2x1dGVQYXRoIHtcbiAgLy8gTk9URTogZm9yIHN1cHBvcnQgbnBtIC8geWFybiAvIHBucG0gd29ya3NwYWNlc1xuICAvLyDtlZjsp4Drp4wgd29ya3NwYWNlIOyTsOuptCBwcm9jZXNzLmN3ZCgpIO2VmOuptCDrkJjripTrjbAuLi4g7J206rG0IOuCmOykkeyXkCDtmJHsnZgg7ZuEIOyImOygle2VmOuKlOqxuOuhnFxuICBjb25zdCB3b3Jrc3BhY2VQYXRoID0gcHJvY2Vzcy5lbnYuUE5QTV9TQ1JJUFRfU1JDX0RJUiA/PyBwcm9jZXNzLmVudi5JTklUX0NXRDtcbiAgaWYgKG5vbk51bGxhYmxlKHdvcmtzcGFjZVBhdGgpKSB7XG4gICAgcmV0dXJuIHdvcmtzcGFjZVBhdGggYXMgQWJzb2x1dGVQYXRoO1xuICB9XG5cbiAgaWYgKG5vbk51bGxhYmxlKHByb2Nlc3MuZW52LlBOUE1fUEFDS0FHRV9OQU1FKSkge1xuICAgIHJldHVybiBwcm9jZXNzLmN3ZCgpLnNwbGl0KHBhdGguc2VwKS5qb2luKHBhdGguc2VwKSBhcyBBYnNvbHV0ZVBhdGg7XG4gIH1cblxuICBjb25zdCBjd2RQYWNrYWdlUGF0aCA9IHBhdGguam9pbihwcm9jZXNzLmN3ZCgpLCBcInBhY2thZ2UuanNvblwiKTtcbiAgaWYgKGZzLmV4aXN0c1N5bmMoY3dkUGFja2FnZVBhdGgpKSB7XG4gICAgcmV0dXJuIHByb2Nlc3MuY3dkKCkuc3BsaXQocGF0aC5zZXApLmpvaW4ocGF0aC5zZXApIGFzIEFic29sdXRlUGF0aDtcbiAgfVxuXG4gIGNvbnN0IGJhc2VQYXRoID0gaW1wb3J0Lm1ldGEuZmlsZW5hbWU7XG4gIGxldCBkaXIgPSBwYXRoLmRpcm5hbWUoYmFzZVBhdGgpO1xuICBpZiAoZGlyLmluY2x1ZGVzKFwiLy55YXJuL1wiKSkge1xuICAgIGRpciA9IGRpci5zcGxpdChcIi8ueWFybi9cIilbMF07XG4gIH1cblxuICBkbyB7XG4gICAgaWYgKGZzLmV4aXN0c1N5bmMocGF0aC5qb2luKGRpciwgXCIvcGFja2FnZS5qc29uXCIpKSkge1xuICAgICAgcmV0dXJuIGRpci5zcGxpdChwYXRoLnNlcCkuam9pbihwYXRoLnNlcCkgYXMgQWJzb2x1dGVQYXRoO1xuICAgIH1cbiAgICBkaXIgPSBkaXIuc3BsaXQocGF0aC5zZXApLnNsaWNlKDAsIC0xKS5qb2luKHBhdGguc2VwKTtcbiAgfSB3aGlsZSAoZGlyLnNwbGl0KHBhdGguc2VwKS5sZW5ndGggPiAxKTtcbiAgdGhyb3cgbmV3IEVycm9yKFwiQ2Fubm90IGZpbmQgQXBwUm9vdCB1c2luZyBTb25hbXUgLTJcIik7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBub25OdWxsYWJsZTxUPih2YWx1ZTogVCk6IHZhbHVlIGlzIE5vbk51bGxhYmxlPFQ+IHtcbiAgcmV0dXJuIHZhbHVlICE9PSBudWxsICYmIHZhbHVlICE9PSB1bmRlZmluZWQ7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBleGhhdXN0aXZlKF9wYXJhbTogbmV2ZXIpIHtcbiAgdGhyb3cgbmV3IEVycm9yKGBleGhhdXN0aXZlYCk7XG59XG5cbi8vIOydvOuwmCDrsoTsoIRcbmV4cG9ydCBmdW5jdGlvbiBhc3NlcnRFeGlzdHM8VD4odmFsdWU6IFQgfCBudWxsIHwgdW5kZWZpbmVkLCBtZXNzYWdlPzogc3RyaW5nKTogVCB7XG4gIGlmICh2YWx1ZSA9PT0gbnVsbCB8fCB2YWx1ZSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKG1lc3NhZ2UgPz8gXCJWYWx1ZSBtdXN0IGV4aXN0XCIpO1xuICB9XG4gIHJldHVybiB2YWx1ZTtcbn1cblxuLy8gbnVsbOunjCDssrTtgaxcbmV4cG9ydCBmdW5jdGlvbiBhc3NlcnROb3ROdWxsPFQ+KHZhbHVlOiBUIHwgbnVsbCwgbWVzc2FnZT86IHN0cmluZyk6IFQge1xuICBpZiAodmFsdWUgPT09IG51bGwpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IobWVzc2FnZSA/PyBcIlZhbHVlIG11c3Qgbm90IGJlIG51bGxcIik7XG4gIH1cbiAgcmV0dXJuIHZhbHVlO1xufVxuLy8gdW5kZWZpbmVk66eMIOyytO2BrFxuZXhwb3J0IGZ1bmN0aW9uIGFzc2VydERlZmluZWQ8VD4odmFsdWU6IFQgfCB1bmRlZmluZWQsIG1lc3NhZ2U/OiBzdHJpbmcpOiBUIHtcbiAgaWYgKHZhbHVlID09PSB1bmRlZmluZWQpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IobWVzc2FnZSA/PyBcIlZhbHVlIG11c3QgYmUgZGVmaW5lZFwiKTtcbiAgfVxuICByZXR1cm4gdmFsdWU7XG59XG5cbi8vIGxvZGFzaCBpbnRlcnNlY3Rpb25CeSDrjIDssrRcbmV4cG9ydCBmdW5jdGlvbiBpbnRlcnNlY3Rpb25CeTxULCBLPihcbiAgYXJyMTogcmVhZG9ubHkgVFtdLFxuICBhcnIyOiByZWFkb25seSBUW10sXG4gIGl0ZXJhdGVlOiAoaXRlbTogVCkgPT4gSyxcbik6IFRbXSB7XG4gIGNvbnN0IGFycjJLZXlzID0gbmV3IFNldChhcnIyLm1hcChpdGVyYXRlZSkpO1xuICByZXR1cm4gYXJyMS5maWx0ZXIoKGl0ZW0pID0+IGFycjJLZXlzLmhhcyhpdGVyYXRlZShpdGVtKSkpO1xufVxuLy8gbG9kYXNoIGRpZmZlcmVuY2VXaXRoIOuMgOyytFxuZXhwb3J0IGZ1bmN0aW9uIGRpZmZlcmVuY2VXaXRoPFQ+KFxuICBhcnIxOiByZWFkb25seSBUW10sXG4gIGFycjI6IHJlYWRvbmx5IFRbXSxcbiAgY29tcGFyYXRvcjogKGE6IFQsIGI6IFQpID0+IGJvb2xlYW4sXG4pOiBUW10ge1xuICByZXR1cm4gYXJyMS5maWx0ZXIoKGl0ZW1BKSA9PiAhYXJyMi5zb21lKChpdGVtQikgPT4gY29tcGFyYXRvcihpdGVtQSwgaXRlbUIpKSk7XG59XG5cbi8vIGJpb21lLWlnbm9yZSBsaW50L3N1c3BpY2lvdXMvbm9FeHBsaWNpdEFueTogZHluYW1pYyBwcm9wZXJ0eSBhY2Nlc3NcbmV4cG9ydCBmdW5jdGlvbiBtZXJnZTxUIGV4dGVuZHMgUmVjb3JkPHN0cmluZywgYW55Pj4oZGVmYXVsdE9iajogVCwgdXNlck9iajogVCk6IFQge1xuICAvLyDsm5Drs7gg67O07KG07J2EIOychO2VtCBkZWZhdWx0T2JqIOuzteyCrFxuICBjb25zdCByZXN1bHQgPSB7IC4uLmRlZmF1bHRPYmogfTtcblxuICAvLyB1c2VyT2Jq7J2YIOqwgSDsho3shLHsnYQg7Iic7ZqMXG4gIGZvciAoY29uc3Qga2V5IGluIHVzZXJPYmopIHtcbiAgICAvLyB1c2VyT2Jq7J2YIG93biBwcm9wZXJ0eeunjCDsspjrpqwgKO2UhOuhnO2GoO2DgOyehSDssrTsnbgg7KCc7Jm4KVxuICAgIGlmIChPYmplY3QuaGFzT3duKHVzZXJPYmosIGtleSkpIHtcbiAgICAgIGNvbnN0IHVzZXJWYWx1ZSA9IHVzZXJPYmpba2V5XTtcbiAgICAgIGNvbnN0IGRlZmF1bHRWYWx1ZSA9IHJlc3VsdFtrZXldO1xuXG4gICAgICAvLyDrkZAg6rCS7J20IOuqqOuRkCDqsJ3ssrTsnbTqs6AsIOuwsOyXtOydtCDslYTri4wg6rK97JqwIOyerOq3gOyggeycvOuhnCDrs5HtlalcbiAgICAgIGlmIChpc1BsYWluT2JqZWN0KHVzZXJWYWx1ZSkgJiYgaXNQbGFpbk9iamVjdChkZWZhdWx0VmFsdWUpKSB7XG4gICAgICAgIHJlc3VsdFtrZXldID0gbWVyZ2UoZGVmYXVsdFZhbHVlLCB1c2VyVmFsdWUpO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgLy8g6re4IOyZuOydmCDqsr3smrAgdXNlck9iauydmCDqsJLsnLzroZwg642u7Ja07JOw6riwXG4gICAgICAgIHJlc3VsdFtrZXldID0gdXNlclZhbHVlO1xuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIHJldHVybiByZXN1bHQ7XG59XG5cbi8vIHBsYWluIG9iamVjdCDtjJDrs4Qg7Zes7Y28IO2VqOyImFxuLy8gKOuwsOyXtCwgbnVsbCwgRGF0ZSDrk7HsnYQg7KCc7Jm47ZWcIOyInOyImCDqsJ3ssrTrp4wgdHJ1ZSlcbmV4cG9ydCBmdW5jdGlvbiBpc1BsYWluT2JqZWN0KHZhbHVlOiB1bmtub3duKTogdmFsdWUgaXMgUmVjb3JkPHN0cmluZywgdW5rbm93bj4ge1xuICByZXR1cm4gKFxuICAgIHZhbHVlICE9PSBudWxsICYmXG4gICAgdHlwZW9mIHZhbHVlID09PSBcIm9iamVjdFwiICYmXG4gICAgIUFycmF5LmlzQXJyYXkodmFsdWUpICYmXG4gICAgT2JqZWN0LnByb3RvdHlwZS50b1N0cmluZy5jYWxsKHZhbHVlKSA9PT0gXCJbb2JqZWN0IE9iamVjdF1cIlxuICApO1xufVxuXG4vLyBDb252ZXJ0IEZhc3RpZnkgaGVhZGVycyB0byBzdGFuZGFyZCBIZWFkZXJzIG9iamVjdFxuZXhwb3J0IGZ1bmN0aW9uIGNvbnZlcnRGYXN0aWZ5SGVhZGVyc1RvU3RhbmRhcmQoaGVhZGVyczogRmFzdGlmeVJlcXVlc3RbXCJoZWFkZXJzXCJdKTogSGVhZGVycyB7XG4gIGNvbnN0IGhlYWRlcnNPYmogPSBuZXcgSGVhZGVycygpO1xuICBPYmplY3QuZW50cmllcyhoZWFkZXJzKS5mb3JFYWNoKChba2V5LCB2YWx1ZV0pID0+IHtcbiAgICBpZiAodmFsdWUpIGhlYWRlcnNPYmouYXBwZW5kKGtleSwgdmFsdWUudG9TdHJpbmcoKSk7XG4gIH0pO1xuICByZXR1cm4gaGVhZGVyc09iajtcbn1cbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFPQSxTQUFnQixrQkFBZ0M7Q0FDOUMsTUFBTSxjQUFjLGlCQUFpQjtBQUNyQyxRQUFPLEtBQUssUUFBUSxZQUFZOztBQUdsQyxTQUFnQixrQkFBZ0M7Q0FHOUMsTUFBTSxnQkFBZ0IsUUFBUSxJQUFJLHVCQUF1QixRQUFRLElBQUk7QUFDckUsS0FBSSxZQUFZLGNBQWMsRUFBRTtBQUM5QixTQUFPOztBQUdULEtBQUksWUFBWSxRQUFRLElBQUksa0JBQWtCLEVBQUU7QUFDOUMsU0FBTyxRQUFRLEtBQUssQ0FBQyxNQUFNLEtBQUssSUFBSSxDQUFDLEtBQUssS0FBSyxJQUFJOztDQUdyRCxNQUFNLGlCQUFpQixLQUFLLEtBQUssUUFBUSxLQUFLLEVBQUUsZUFBZTtBQUMvRCxLQUFJLEdBQUcsV0FBVyxlQUFlLEVBQUU7QUFDakMsU0FBTyxRQUFRLEtBQUssQ0FBQyxNQUFNLEtBQUssSUFBSSxDQUFDLEtBQUssS0FBSyxJQUFJOztDQUdyRCxNQUFNLFdBQVcsT0FBTyxLQUFLO0NBQzdCLElBQUksTUFBTSxLQUFLLFFBQVEsU0FBUztBQUNoQyxLQUFJLElBQUksU0FBUyxVQUFVLEVBQUU7QUFDM0IsUUFBTSxJQUFJLE1BQU0sVUFBVSxDQUFDOztBQUc3QixJQUFHO0FBQ0QsTUFBSSxHQUFHLFdBQVcsS0FBSyxLQUFLLEtBQUssZ0JBQWdCLENBQUMsRUFBRTtBQUNsRCxVQUFPLElBQUksTUFBTSxLQUFLLElBQUksQ0FBQyxLQUFLLEtBQUssSUFBSTs7QUFFM0MsUUFBTSxJQUFJLE1BQU0sS0FBSyxJQUFJLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDLEtBQUssS0FBSyxJQUFJO1VBQzlDLElBQUksTUFBTSxLQUFLLElBQUksQ0FBQyxTQUFTO0FBQ3RDLE9BQU0sSUFBSSxNQUFNLHNDQUFzQzs7QUFHeEQsU0FBZ0IsWUFBZSxPQUFtQztBQUNoRSxRQUFPLFVBQVUsUUFBUSxVQUFVOztBQUdyQyxTQUFnQixXQUFXLFFBQWU7QUFDeEMsT0FBTSxJQUFJLE1BQU0sYUFBYTs7QUFJL0IsU0FBZ0IsYUFBZ0IsT0FBNkIsU0FBcUI7QUFDaEYsS0FBSSxVQUFVLFFBQVEsVUFBVSxXQUFXO0FBQ3pDLFFBQU0sSUFBSSxNQUFNLFdBQVcsbUJBQW1COztBQUVoRCxRQUFPOztBQUlULFNBQWdCLGNBQWlCLE9BQWlCLFNBQXFCO0FBQ3JFLEtBQUksVUFBVSxNQUFNO0FBQ2xCLFFBQU0sSUFBSSxNQUFNLFdBQVcseUJBQXlCOztBQUV0RCxRQUFPOztBQUdULFNBQWdCLGNBQWlCLE9BQXNCLFNBQXFCO0FBQzFFLEtBQUksVUFBVSxXQUFXO0FBQ3ZCLFFBQU0sSUFBSSxNQUFNLFdBQVcsd0JBQXdCOztBQUVyRCxRQUFPOztBQUlULFNBQWdCLGVBQ2QsTUFDQSxNQUNBLFVBQ0s7Q0FDTCxNQUFNLFdBQVcsSUFBSSxJQUFJLEtBQUssSUFBSSxTQUFTLENBQUM7QUFDNUMsUUFBTyxLQUFLLFFBQVEsU0FBUyxTQUFTLElBQUksU0FBUyxLQUFLLENBQUMsQ0FBQzs7QUFHNUQsU0FBZ0IsZUFDZCxNQUNBLE1BQ0EsWUFDSztBQUNMLFFBQU8sS0FBSyxRQUFRLFVBQVUsQ0FBQyxLQUFLLE1BQU0sVUFBVSxXQUFXLE9BQU8sTUFBTSxDQUFDLENBQUM7O0FBSWhGLFNBQWdCLE1BQXFDLFlBQWUsU0FBZTtDQUVqRixNQUFNLFNBQVMsRUFBRSxHQUFHLFlBQVk7QUFHaEMsTUFBSyxNQUFNLE9BQU8sU0FBUztBQUV6QixNQUFJLE9BQU8sT0FBTyxTQUFTLElBQUksRUFBRTtHQUMvQixNQUFNLFlBQVksUUFBUTtHQUMxQixNQUFNLGVBQWUsT0FBTztBQUc1QixPQUFJLGNBQWMsVUFBVSxJQUFJLGNBQWMsYUFBYSxFQUFFO0FBQzNELFdBQU8sT0FBTyxNQUFNLGNBQWMsVUFBVTtVQUN2QztBQUVMLFdBQU8sT0FBTzs7OztBQUtwQixRQUFPOztBQUtULFNBQWdCLGNBQWMsT0FBa0Q7QUFDOUUsUUFDRSxVQUFVLFFBQ1YsT0FBTyxVQUFVLFlBQ2pCLENBQUMsTUFBTSxRQUFRLE1BQU0sSUFDckIsT0FBTyxVQUFVLFNBQVMsS0FBSyxNQUFNLEtBQUs7O0FBSzlDLFNBQWdCLGdDQUFnQyxTQUE2QztDQUMzRixNQUFNLGFBQWEsSUFBSSxTQUFTO0FBQ2hDLFFBQU8sUUFBUSxRQUFRLENBQUMsU0FBUyxDQUFDLEtBQUssV0FBVztBQUNoRCxNQUFJLE1BQU8sWUFBVyxPQUFPLEtBQUssTUFBTSxVQUFVLENBQUM7R0FDbkQ7QUFDRixRQUFPIn0=
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonamu",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.7",
|
|
4
4
|
"description": "Sonamu — TypeScript Fullstack API Framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -95,11 +95,13 @@
|
|
|
95
95
|
"@fastify/multipart": "^8",
|
|
96
96
|
"@fastify/passport": "^2.2.0",
|
|
97
97
|
"@fastify/static": "^7",
|
|
98
|
+
"@fastify/websocket": "^8.3.0",
|
|
98
99
|
"@logtape/fastify": "2.0.0",
|
|
99
100
|
"@logtape/logtape": "2.0.0",
|
|
100
101
|
"@logtape/pretty": "2.0.0",
|
|
101
102
|
"@logtape/redaction": "2.0.0",
|
|
102
103
|
"@sheetkit/node": "^0.5.0",
|
|
104
|
+
"@types/ws": "^8.18.1",
|
|
103
105
|
"bcrypt": "^6.0.0",
|
|
104
106
|
"bentocache": "^1.5.0",
|
|
105
107
|
"better-auth": "~1.6.0",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { type AnyWebSocketConnection } from "../../stream/ws";
|
|
4
|
+
import { type WebSocketContext } from "../context";
|
|
5
|
+
import { Sonamu } from "../sonamu";
|
|
6
|
+
|
|
7
|
+
describe("Sonamu websocket context scoping", () => {
|
|
8
|
+
it("restores websocket context inside deferred message handlers", async () => {
|
|
9
|
+
const messageHandlers = new Map<string, (data: unknown) => void | Promise<void>>();
|
|
10
|
+
|
|
11
|
+
const rawWs = {
|
|
12
|
+
id: "ws-1",
|
|
13
|
+
namespace: "chat",
|
|
14
|
+
transport: "ws" as const,
|
|
15
|
+
closed: false,
|
|
16
|
+
publishUntyped() {},
|
|
17
|
+
close() {},
|
|
18
|
+
onClose() {},
|
|
19
|
+
onMessage(event, handler) {
|
|
20
|
+
messageHandlers.set(String(event), handler as (data: unknown) => void | Promise<void>);
|
|
21
|
+
},
|
|
22
|
+
publish() {},
|
|
23
|
+
waitForClose() {
|
|
24
|
+
return Promise.resolve();
|
|
25
|
+
},
|
|
26
|
+
join() {},
|
|
27
|
+
leave() {},
|
|
28
|
+
setUserId() {},
|
|
29
|
+
clearUserId() {},
|
|
30
|
+
} satisfies AnyWebSocketConnection;
|
|
31
|
+
|
|
32
|
+
let context: WebSocketContext | null = null;
|
|
33
|
+
const scopedWs = (
|
|
34
|
+
Sonamu as unknown as {
|
|
35
|
+
createScopedWebSocketConnection(
|
|
36
|
+
ws: AnyWebSocketConnection,
|
|
37
|
+
getContext: () => WebSocketContext | null,
|
|
38
|
+
): AnyWebSocketConnection;
|
|
39
|
+
}
|
|
40
|
+
).createScopedWebSocketConnection(rawWs, () => context);
|
|
41
|
+
|
|
42
|
+
context = {
|
|
43
|
+
transport: "ws",
|
|
44
|
+
request: {} as WebSocketContext["request"],
|
|
45
|
+
headers: {},
|
|
46
|
+
ws: scopedWs,
|
|
47
|
+
naiteStore: new Map(),
|
|
48
|
+
locale: "ko",
|
|
49
|
+
user: null,
|
|
50
|
+
session: null,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let seenTransport: WebSocketContext["transport"] | null = null;
|
|
54
|
+
scopedWs.onMessage("joinRoom", async () => {
|
|
55
|
+
seenTransport = Sonamu.getContext<WebSocketContext>().transport;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await messageHandlers.get("joinRoom")?.({
|
|
59
|
+
roomId: "room-1",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(seenTransport).toBe("ws");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expectTypeOf, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { type WebSocketContext } from "../context";
|
|
4
|
+
|
|
5
|
+
type ChatOutEvents = {
|
|
6
|
+
ready: {
|
|
7
|
+
history: string[];
|
|
8
|
+
};
|
|
9
|
+
typingUsers: Array<{
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
}>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type ChatInEvents = {
|
|
16
|
+
sendMessage: {
|
|
17
|
+
content: string;
|
|
18
|
+
};
|
|
19
|
+
typing: {
|
|
20
|
+
active: boolean;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function assertTypedWebSocketContext(ctx: WebSocketContext<ChatOutEvents, ChatInEvents>) {
|
|
25
|
+
ctx.ws.onMessage("sendMessage", (data) => {
|
|
26
|
+
expectTypeOf(data).toEqualTypeOf<{ content: string }>();
|
|
27
|
+
expectTypeOf(data.content).toEqualTypeOf<string>();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
ctx.ws.onMessage("typing", (data) => {
|
|
31
|
+
expectTypeOf(data).toEqualTypeOf<{ active: boolean }>();
|
|
32
|
+
expectTypeOf(data.active).toEqualTypeOf<boolean>();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
ctx.ws.publish("ready", { history: ["hello"] });
|
|
36
|
+
ctx.ws.publish("typingUsers", [{ id: "u1", name: "Kim" }]);
|
|
37
|
+
|
|
38
|
+
// @ts-expect-error invalid inbound event name
|
|
39
|
+
ctx.ws.onMessage("unknown", () => {});
|
|
40
|
+
// @ts-expect-error invalid outbound payload
|
|
41
|
+
ctx.ws.publish("ready", { nope: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function assertDefaultWebSocketContext(ctx: WebSocketContext) {
|
|
45
|
+
ctx.ws.onMessage("sendMessage", (data) => {
|
|
46
|
+
expectTypeOf(data).toEqualTypeOf<unknown>();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("WebSocketContext typing", () => {
|
|
51
|
+
it("narrows ws handlers and publish payloads from generic event maps", () => {
|
|
52
|
+
expectTypeOf(assertTypedWebSocketContext).toBeFunction();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("defaults handler payloads to unknown when generics are omitted", () => {
|
|
56
|
+
expectTypeOf(assertDefaultWebSocketContext).toBeFunction();
|
|
57
|
+
});
|
|
58
|
+
});
|
package/src/api/config.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type FastifyCorsOptions } from "@fastify/cors";
|
|
|
3
3
|
import { type FastifyFormbodyOptions } from "@fastify/formbody";
|
|
4
4
|
import { type FastifyMultipartOptions } from "@fastify/multipart";
|
|
5
5
|
import { type FastifyStaticOptions } from "@fastify/static";
|
|
6
|
+
import { type WebsocketPluginOptions } from "@fastify/websocket";
|
|
6
7
|
import { type BetterAuthOptions } from "better-auth";
|
|
7
8
|
import {
|
|
8
9
|
type FastifyInstance,
|
|
@@ -18,9 +19,10 @@ import { type CacheConfig } from "../cache/types";
|
|
|
18
19
|
import { type SonamuDBConfig } from "../database/db";
|
|
19
20
|
import { type SonamuLoggingOptions } from "../logger/configure";
|
|
20
21
|
import { type StorageConfig } from "../storage/types";
|
|
22
|
+
import { type WebSocketRuntimeOptions } from "../stream/ws";
|
|
21
23
|
import { type WorkflowOptions } from "../tasks/workflow-manager";
|
|
22
24
|
import { type Executable, type SonamuFastifyConfig } from "../types/types";
|
|
23
|
-
import { type Context } from "./context";
|
|
25
|
+
import { type Context, type WebSocketContext } from "./context";
|
|
24
26
|
|
|
25
27
|
export type DatabaseConfig = Omit<Knex.Config, "connection"> & {
|
|
26
28
|
connection?: Knex.PgConnectionConfig;
|
|
@@ -180,6 +182,7 @@ export type SonamuServerOptions = {
|
|
|
180
182
|
qs?: boolean | QsPluginOptions;
|
|
181
183
|
sse?: boolean | SsePluginOptions;
|
|
182
184
|
static?: boolean | FastifyStaticOptions;
|
|
185
|
+
ws?: boolean | WebsocketPluginOptions;
|
|
183
186
|
|
|
184
187
|
custom?: (server: FastifyInstance) => void;
|
|
185
188
|
};
|
|
@@ -200,6 +203,14 @@ export type SonamuServerOptions = {
|
|
|
200
203
|
};
|
|
201
204
|
};
|
|
202
205
|
|
|
206
|
+
/**
|
|
207
|
+
* WebSocket runtime 설정.
|
|
208
|
+
*
|
|
209
|
+
* 단일 인스턴스에서는 기본값으로 충분하며, 멀티 인스턴스/대규모 환경에서는
|
|
210
|
+
* presence store와 cluster bus를 여기서 주입합니다.
|
|
211
|
+
*/
|
|
212
|
+
websocket?: WebSocketRuntimeOptions;
|
|
213
|
+
|
|
203
214
|
apiConfig: SonamuFastifyConfig;
|
|
204
215
|
|
|
205
216
|
/**
|
|
@@ -255,9 +266,24 @@ export type SonamuTaskOptions = {
|
|
|
255
266
|
contextProvider: (
|
|
256
267
|
defaultContext: Pick<
|
|
257
268
|
Context,
|
|
258
|
-
|
|
269
|
+
| "transport"
|
|
270
|
+
| "reply"
|
|
271
|
+
| "request"
|
|
272
|
+
| "headers"
|
|
273
|
+
| "createSSE"
|
|
274
|
+
| "naiteStore"
|
|
275
|
+
| "locale"
|
|
276
|
+
| "user"
|
|
277
|
+
| "session"
|
|
259
278
|
>,
|
|
260
279
|
) => Context | Promise<Context>;
|
|
280
|
+
websocketContextProvider?: (
|
|
281
|
+
defaultContext: Pick<
|
|
282
|
+
WebSocketContext,
|
|
283
|
+
"transport" | "request" | "headers" | "ws" | "naiteStore" | "locale" | "user" | "session"
|
|
284
|
+
>,
|
|
285
|
+
request: FastifyRequest,
|
|
286
|
+
) => WebSocketContext | Promise<WebSocketContext>;
|
|
261
287
|
};
|
|
262
288
|
|
|
263
289
|
export type SonamuSSROptions = {
|
package/src/api/context.ts
CHANGED
|
@@ -14,24 +14,38 @@ import { type NaiteStore } from "../naite/naite";
|
|
|
14
14
|
import { type BufferedFile } from "../storage/buffered-file";
|
|
15
15
|
import { type UploadedFile } from "../storage/uploaded-file";
|
|
16
16
|
import { type createSSEFactory } from "../stream/sse";
|
|
17
|
+
import { type WebSocketConnection, type WebSocketEventMap } from "../stream/ws";
|
|
17
18
|
|
|
18
19
|
// oxlint-disable-next-line @typescript-eslint/no-empty-interface -- Context 확장 타입
|
|
19
20
|
export interface ContextExtend {}
|
|
20
|
-
|
|
21
|
+
type BaseContext = {
|
|
21
22
|
request: FastifyRequest;
|
|
22
|
-
reply: FastifyReply<Server, IncomingMessage, ServerResponse, RouteGenericInterface>;
|
|
23
23
|
headers: IncomingHttpHeaders;
|
|
24
|
-
createSSE: <T extends ZodObject>(events: T) => ReturnType<typeof createSSEFactory<T>>;
|
|
25
24
|
naiteStore: NaiteStore;
|
|
26
25
|
/** 현재 요청의 locale */
|
|
27
26
|
locale: string;
|
|
27
|
+
/** 현재 로그인한 사용자 (null이면 미인증) */
|
|
28
|
+
user: User | null;
|
|
29
|
+
/** 현재 세션 정보 (null이면 미인증) */
|
|
30
|
+
session: Session | null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type Context = BaseContext & {
|
|
34
|
+
transport: "http";
|
|
35
|
+
reply: FastifyReply<Server, IncomingMessage, ServerResponse, RouteGenericInterface>;
|
|
36
|
+
createSSE: <T extends ZodObject>(events: T) => ReturnType<typeof createSSEFactory<T>>;
|
|
28
37
|
/** buffer 모드에서 업로드된 파일 */
|
|
29
38
|
bufferedFiles?: BufferedFile[];
|
|
30
39
|
/** stream 모드에서 업로드된 파일 */
|
|
31
40
|
uploadedFiles?: UploadedFile[];
|
|
41
|
+
} & ContextExtend;
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
export type WebSocketContext<
|
|
44
|
+
TOut extends WebSocketEventMap = WebSocketEventMap,
|
|
45
|
+
TIn extends WebSocketEventMap = WebSocketEventMap,
|
|
46
|
+
> = BaseContext & {
|
|
47
|
+
transport: "ws";
|
|
48
|
+
ws: WebSocketConnection<TOut, TIn>;
|
|
37
49
|
} & ContextExtend;
|
|
50
|
+
|
|
51
|
+
export type RuntimeContext = Context | WebSocketContext;
|
package/src/api/decorators.ts
CHANGED
|
@@ -53,7 +53,7 @@ export type ApiDecoratorOptions = {
|
|
|
53
53
|
compress?: CompressConfig;
|
|
54
54
|
};
|
|
55
55
|
export type StreamDecoratorOptions = {
|
|
56
|
-
type: "sse";
|
|
56
|
+
type: "sse";
|
|
57
57
|
// oxlint-disable-next-line @typescript-eslint/no-explicit-any -- 이벤트 키별로 넘겨주는 값이므로 어떤 타입이든 상관없음
|
|
58
58
|
events: z.ZodObject<any>;
|
|
59
59
|
path?: string;
|
|
@@ -61,6 +61,24 @@ export type StreamDecoratorOptions = {
|
|
|
61
61
|
guards?: GuardKey[];
|
|
62
62
|
description?: string;
|
|
63
63
|
};
|
|
64
|
+
export type WebSocketDecoratorOptions = {
|
|
65
|
+
// oxlint-disable-next-line @typescript-eslint/no-explicit-any -- 이벤트 키별로 넘겨주는 값이므로 어떤 타입이든 상관없음
|
|
66
|
+
outEvents: z.ZodObject<any>;
|
|
67
|
+
// oxlint-disable-next-line @typescript-eslint/no-explicit-any -- 이벤트 키별로 넘겨주는 값이므로 어떤 타입이든 상관없음
|
|
68
|
+
inEvents: z.ZodObject<any>;
|
|
69
|
+
path?: string;
|
|
70
|
+
resourceName?: string;
|
|
71
|
+
guards?: GuardKey[];
|
|
72
|
+
description?: string;
|
|
73
|
+
heartbeat?: number;
|
|
74
|
+
maxPayload?: number;
|
|
75
|
+
namespace?: string;
|
|
76
|
+
};
|
|
77
|
+
export type ResolvedWebSocketDecoratorOptions = WebSocketDecoratorOptions & {
|
|
78
|
+
// codegen이 타입 이름을 재사용할 수 있도록 syncer가 AST에서 보강하는 메타데이터
|
|
79
|
+
outEventsTypeRef?: ApiParamType.Ref;
|
|
80
|
+
inEventsTypeRef?: ApiParamType.Ref;
|
|
81
|
+
};
|
|
64
82
|
|
|
65
83
|
type BufferUploadOptions = {
|
|
66
84
|
consume?: "buffer";
|
|
@@ -85,6 +103,7 @@ export const registeredApis: {
|
|
|
85
103
|
path: string;
|
|
86
104
|
options: ApiDecoratorOptions;
|
|
87
105
|
streamOptions?: StreamDecoratorOptions;
|
|
106
|
+
websocketOptions?: ResolvedWebSocketDecoratorOptions;
|
|
88
107
|
uploadOptions?: UploadDecoratorOptions;
|
|
89
108
|
}[] = [];
|
|
90
109
|
export type ExtendedApi = {
|
|
@@ -93,6 +112,7 @@ export type ExtendedApi = {
|
|
|
93
112
|
path: string;
|
|
94
113
|
options: ApiDecoratorOptions;
|
|
95
114
|
streamOptions?: StreamDecoratorOptions;
|
|
115
|
+
websocketOptions?: ResolvedWebSocketDecoratorOptions;
|
|
96
116
|
uploadOptions?: UploadDecoratorOptions;
|
|
97
117
|
typeParameters: ApiParamType.TypeParam[];
|
|
98
118
|
parameters: ApiParam[];
|
|
@@ -103,6 +123,7 @@ type DecoratorTarget = { constructor: { name: string } };
|
|
|
103
123
|
const DECORATOR_TYPES = {
|
|
104
124
|
API: Symbol("api"),
|
|
105
125
|
STREAM: Symbol("stream"),
|
|
126
|
+
WEBSOCKET: Symbol("websocket"),
|
|
106
127
|
UPLOAD: Symbol("upload"),
|
|
107
128
|
} as const;
|
|
108
129
|
|
|
@@ -110,7 +131,7 @@ function checkSingleDecorator(target: DecoratorTarget, propertyKey: string, deco
|
|
|
110
131
|
const method = target[propertyKey as keyof typeof target] as { __decoratorType?: symbol };
|
|
111
132
|
if (method?.__decoratorType && method?.__decoratorType !== decoratorType) {
|
|
112
133
|
throw new Error(
|
|
113
|
-
`@${decoratorType.description ?? String(decoratorType)} decorator can only be used once on ${target.constructor.name}.${propertyKey}. You can use only one of @api or @
|
|
134
|
+
`@${decoratorType.description ?? String(decoratorType)} decorator can only be used once on ${target.constructor.name}.${propertyKey}. You can use only one of @api, @stream, @websocket, or @upload decorator on the same method.`,
|
|
114
135
|
);
|
|
115
136
|
} else {
|
|
116
137
|
method.__decoratorType = decoratorType;
|
|
@@ -278,6 +299,85 @@ export function stream(options: StreamDecoratorOptions) {
|
|
|
278
299
|
};
|
|
279
300
|
}
|
|
280
301
|
|
|
302
|
+
export function websocket(options: WebSocketDecoratorOptions) {
|
|
303
|
+
return (target: DecoratorTarget, propertyKey: string, descriptor: PropertyDescriptor) => {
|
|
304
|
+
const modelName = target.constructor.name.match(/(.+)Class$/)?.[1];
|
|
305
|
+
assert(
|
|
306
|
+
modelName,
|
|
307
|
+
`modelName is required on @websocket decorator on ${target.constructor.name}.${propertyKey}`,
|
|
308
|
+
);
|
|
309
|
+
const methodName = propertyKey;
|
|
310
|
+
|
|
311
|
+
checkSingleDecorator(target, propertyKey, DECORATOR_TYPES.WEBSOCKET);
|
|
312
|
+
|
|
313
|
+
const defaultPath = `/${inflection.camelize(
|
|
314
|
+
modelName.replace(/Model$/, "").replace(/Frame$/, ""),
|
|
315
|
+
true,
|
|
316
|
+
)}/${inflection.camelize(propertyKey, true)}`;
|
|
317
|
+
const path = options.path ?? defaultPath;
|
|
318
|
+
const { outEvents: _outEvents, inEvents: _inEvents, ...apiOptions } = options;
|
|
319
|
+
const optionsWithDefaults = {
|
|
320
|
+
...apiOptions,
|
|
321
|
+
httpMethod: "GET" as HTTPMethods,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const existingApi = registeredApis.find(
|
|
325
|
+
(api) => api.modelName === modelName && api.methodName === methodName,
|
|
326
|
+
);
|
|
327
|
+
if (existingApi) {
|
|
328
|
+
assertNoConflictingPath("websocket", modelName, methodName, existingApi.path, path);
|
|
329
|
+
existingApi.path = path;
|
|
330
|
+
|
|
331
|
+
assertNoConflictingOptions(
|
|
332
|
+
"websocket",
|
|
333
|
+
modelName,
|
|
334
|
+
methodName,
|
|
335
|
+
existingApi.options,
|
|
336
|
+
optionsWithDefaults,
|
|
337
|
+
);
|
|
338
|
+
existingApi.options = {
|
|
339
|
+
...existingApi.options,
|
|
340
|
+
...optionsWithDefaults,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
existingApi.websocketOptions = options;
|
|
344
|
+
} else {
|
|
345
|
+
registeredApis.push({
|
|
346
|
+
modelName,
|
|
347
|
+
methodName,
|
|
348
|
+
path,
|
|
349
|
+
options: optionsWithDefaults,
|
|
350
|
+
websocketOptions: options,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const originalMethod = descriptor.value;
|
|
355
|
+
descriptor.value = async function (this: BaseModelClass | BaseFrameClass, ...args: unknown[]) {
|
|
356
|
+
if (this instanceof BaseModelClass) {
|
|
357
|
+
getLogger(convertDomainToCategory(this.modelName, "model")).debug(
|
|
358
|
+
"websocket: {model}.{method}",
|
|
359
|
+
{
|
|
360
|
+
model: modelName,
|
|
361
|
+
method: methodName,
|
|
362
|
+
},
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (this instanceof BaseFrameClass) {
|
|
367
|
+
getLogger(convertDomainToCategory(this.frameName, "frame")).debug(
|
|
368
|
+
"websocket: {model}.{method}",
|
|
369
|
+
{
|
|
370
|
+
model: modelName,
|
|
371
|
+
method: methodName,
|
|
372
|
+
},
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return originalMethod.apply(this, args);
|
|
377
|
+
};
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
281
381
|
export function transactional(options: TransactionalOptions = {}) {
|
|
282
382
|
const { isolation, readOnly, dbPreset = "w" } = options;
|
|
283
383
|
|
|
@@ -286,7 +386,7 @@ export function transactional(options: TransactionalOptions = {}) {
|
|
|
286
386
|
const modelName = target.constructor.name.match(/(.+)Class$/)?.[1];
|
|
287
387
|
assert(
|
|
288
388
|
modelName,
|
|
289
|
-
`modelName is required on @
|
|
389
|
+
`modelName is required on @transactional decorator on ${target.constructor.name}.${propertyKey}`,
|
|
290
390
|
);
|
|
291
391
|
const methodName = propertyKey;
|
|
292
392
|
|