velocious 1.0.429 → 1.0.431
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/bin/velocious.js +48 -0
- package/build/bin/velocious.js +39 -34
- package/build/index.js +1 -2
- package/build/src/application.js +214 -187
- package/build/src/authorization/ability.d.ts +24 -23
- package/build/src/authorization/ability.d.ts.map +1 -1
- package/build/src/authorization/ability.js +300 -252
- package/build/src/authorization/base-resource.d.ts +20 -26
- package/build/src/authorization/base-resource.d.ts.map +1 -1
- package/build/src/authorization/base-resource.js +136 -118
- package/build/src/background-jobs/client.js +47 -43
- package/build/src/background-jobs/cron-expression.js +166 -127
- package/build/src/background-jobs/forked-runner-child.js +47 -37
- package/build/src/background-jobs/job-record.js +10 -8
- package/build/src/background-jobs/job-registry.js +84 -72
- package/build/src/background-jobs/job-runner.js +81 -74
- package/build/src/background-jobs/job.js +72 -62
- package/build/src/background-jobs/json-socket.js +70 -65
- package/build/src/background-jobs/main.js +900 -841
- package/build/src/background-jobs/normalize-error.js +11 -12
- package/build/src/background-jobs/scheduler.js +247 -205
- package/build/src/background-jobs/socket-request.js +65 -60
- package/build/src/background-jobs/status-reporter.js +96 -86
- package/build/src/background-jobs/store.js +980 -862
- package/build/src/background-jobs/types.js +3 -2
- package/build/src/background-jobs/web/authorization.js +50 -38
- package/build/src/background-jobs/web/controller.js +268 -232
- package/build/src/background-jobs/web/index.js +40 -36
- package/build/src/background-jobs/web/path-matcher.js +48 -45
- package/build/src/background-jobs/web/registry.js +14 -9
- package/build/src/background-jobs/worker.js +639 -585
- package/build/src/beacon/client.js +293 -264
- package/build/src/beacon/in-process-broker.js +25 -20
- package/build/src/beacon/in-process-client.js +116 -104
- package/build/src/beacon/server.js +126 -110
- package/build/src/beacon/types.js +8 -2
- package/build/src/cli/base-command.js +57 -49
- package/build/src/cli/browser-cli.js +42 -37
- package/build/src/cli/commands/background-jobs-main.js +5 -5
- package/build/src/cli/commands/background-jobs-runner.js +5 -5
- package/build/src/cli/commands/background-jobs-worker.js +5 -5
- package/build/src/cli/commands/beacon.js +5 -5
- package/build/src/cli/commands/console.js +10 -10
- package/build/src/cli/commands/db/base-command.js +76 -71
- package/build/src/cli/commands/db/create.js +61 -53
- package/build/src/cli/commands/db/drop.js +71 -62
- package/build/src/cli/commands/db/migrate.js +15 -13
- package/build/src/cli/commands/db/reset.js +19 -16
- package/build/src/cli/commands/db/rollback.js +13 -12
- package/build/src/cli/commands/db/schema/dump.js +9 -9
- package/build/src/cli/commands/db/schema/load.js +9 -9
- package/build/src/cli/commands/db/seed.js +9 -9
- package/build/src/cli/commands/db/tenants/check.js +35 -32
- package/build/src/cli/commands/db/tenants/create.js +29 -26
- package/build/src/cli/commands/db/tenants/migrate.js +44 -40
- package/build/src/cli/commands/destroy/migration.js +5 -5
- package/build/src/cli/commands/generate/base-models.js +5 -5
- package/build/src/cli/commands/generate/frontend-models.js +9 -9
- package/build/src/cli/commands/generate/migration.js +5 -5
- package/build/src/cli/commands/generate/model.js +5 -5
- package/build/src/cli/commands/init.js +9 -7
- package/build/src/cli/commands/routes.js +6 -6
- package/build/src/cli/commands/run-script.js +9 -9
- package/build/src/cli/commands/runner.js +9 -9
- package/build/src/cli/commands/server.js +6 -6
- package/build/src/cli/commands/test.js +7 -6
- package/build/src/cli/index.js +141 -127
- package/build/src/cli/tenant-database-command-helper.js +185 -154
- package/build/src/cli/use-browser-cli.js +20 -15
- package/build/src/configuration-resolver.js +54 -47
- package/build/src/configuration-types.d.ts +21 -2
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +60 -3
- package/build/src/configuration.js +2547 -2240
- package/build/src/controller.js +407 -363
- package/build/src/current-configuration.js +12 -9
- package/build/src/current.js +75 -70
- package/build/src/database/annotations-async-hooks.js +22 -16
- package/build/src/database/annotations.js +18 -12
- package/build/src/database/drivers/base-column.js +179 -155
- package/build/src/database/drivers/base-columns-index.js +78 -69
- package/build/src/database/drivers/base-foreign-key.js +101 -89
- package/build/src/database/drivers/base-table.js +149 -124
- package/build/src/database/drivers/base.js +1489 -1306
- package/build/src/database/drivers/mssql/column.js +50 -39
- package/build/src/database/drivers/mssql/columns-index.js +3 -2
- package/build/src/database/drivers/mssql/connect-connection.js +9 -11
- package/build/src/database/drivers/mssql/foreign-key.js +9 -8
- package/build/src/database/drivers/mssql/index.js +587 -507
- package/build/src/database/drivers/mssql/options.js +75 -68
- package/build/src/database/drivers/mssql/query-parser.js +3 -2
- package/build/src/database/drivers/mssql/sql/alter-table.js +2 -2
- package/build/src/database/drivers/mssql/sql/create-database.js +31 -24
- package/build/src/database/drivers/mssql/sql/create-index.js +2 -2
- package/build/src/database/drivers/mssql/sql/create-table.js +2 -2
- package/build/src/database/drivers/mssql/sql/delete.js +16 -14
- package/build/src/database/drivers/mssql/sql/drop-database.js +31 -24
- package/build/src/database/drivers/mssql/sql/drop-table.js +2 -2
- package/build/src/database/drivers/mssql/sql/insert.js +2 -2
- package/build/src/database/drivers/mssql/sql/update.js +28 -24
- package/build/src/database/drivers/mssql/sql/upsert.js +20 -18
- package/build/src/database/drivers/mssql/structure-sql.js +114 -102
- package/build/src/database/drivers/mssql/table.js +96 -81
- package/build/src/database/drivers/mysql/column.js +92 -75
- package/build/src/database/drivers/mysql/columns-index.js +19 -16
- package/build/src/database/drivers/mysql/foreign-key.js +9 -8
- package/build/src/database/drivers/mysql/index.js +457 -396
- package/build/src/database/drivers/mysql/options.js +30 -26
- package/build/src/database/drivers/mysql/query-parser.js +3 -2
- package/build/src/database/drivers/mysql/query.js +29 -26
- package/build/src/database/drivers/mysql/sql/alter-table.js +3 -2
- package/build/src/database/drivers/mysql/sql/create-database.js +28 -23
- package/build/src/database/drivers/mysql/sql/create-index.js +3 -2
- package/build/src/database/drivers/mysql/sql/create-table.js +3 -2
- package/build/src/database/drivers/mysql/sql/delete.js +17 -14
- package/build/src/database/drivers/mysql/sql/drop-database.js +3 -2
- package/build/src/database/drivers/mysql/sql/drop-table.js +3 -2
- package/build/src/database/drivers/mysql/sql/insert.js +3 -2
- package/build/src/database/drivers/mysql/sql/update.js +29 -24
- package/build/src/database/drivers/mysql/sql/upsert.js +10 -8
- package/build/src/database/drivers/mysql/structure-sql.js +88 -79
- package/build/src/database/drivers/mysql/table.js +98 -83
- package/build/src/database/drivers/pgsql/column.js +72 -56
- package/build/src/database/drivers/pgsql/columns-index.js +3 -2
- package/build/src/database/drivers/pgsql/foreign-key.js +9 -8
- package/build/src/database/drivers/pgsql/index.js +438 -377
- package/build/src/database/drivers/pgsql/options.js +28 -25
- package/build/src/database/drivers/pgsql/query-parser.js +3 -2
- package/build/src/database/drivers/pgsql/sql/alter-table.js +3 -2
- package/build/src/database/drivers/pgsql/sql/create-database.js +23 -19
- package/build/src/database/drivers/pgsql/sql/create-index.js +3 -2
- package/build/src/database/drivers/pgsql/sql/create-table.js +3 -2
- package/build/src/database/drivers/pgsql/sql/delete.js +17 -14
- package/build/src/database/drivers/pgsql/sql/drop-database.js +3 -2
- package/build/src/database/drivers/pgsql/sql/drop-table.js +3 -2
- package/build/src/database/drivers/pgsql/sql/insert.js +3 -2
- package/build/src/database/drivers/pgsql/sql/update.js +29 -24
- package/build/src/database/drivers/pgsql/sql/upsert.js +11 -9
- package/build/src/database/drivers/pgsql/structure-sql.js +120 -108
- package/build/src/database/drivers/pgsql/table.js +77 -60
- package/build/src/database/drivers/sqlite/base.js +478 -405
- package/build/src/database/drivers/sqlite/column.js +69 -54
- package/build/src/database/drivers/sqlite/columns-index.js +27 -22
- package/build/src/database/drivers/sqlite/connection-sql-js.js +42 -35
- package/build/src/database/drivers/sqlite/foreign-key.js +21 -18
- package/build/src/database/drivers/sqlite/index.js +373 -330
- package/build/src/database/drivers/sqlite/index.native.js +64 -55
- package/build/src/database/drivers/sqlite/index.web.js +87 -69
- package/build/src/database/drivers/sqlite/options.js +28 -25
- package/build/src/database/drivers/sqlite/query-parser.js +3 -2
- package/build/src/database/drivers/sqlite/query.js +24 -21
- package/build/src/database/drivers/sqlite/query.native.js +25 -20
- package/build/src/database/drivers/sqlite/query.web.js +37 -30
- package/build/src/database/drivers/sqlite/sql/alter-table.js +179 -159
- package/build/src/database/drivers/sqlite/sql/create-index.js +3 -2
- package/build/src/database/drivers/sqlite/sql/create-table.js +3 -2
- package/build/src/database/drivers/sqlite/sql/delete.js +22 -17
- package/build/src/database/drivers/sqlite/sql/drop-table.js +3 -2
- package/build/src/database/drivers/sqlite/sql/insert.js +3 -2
- package/build/src/database/drivers/sqlite/sql/update.js +29 -24
- package/build/src/database/drivers/sqlite/sql/upsert.js +11 -9
- package/build/src/database/drivers/sqlite/structure-sql.js +52 -49
- package/build/src/database/drivers/sqlite/table-rebuilder.js +75 -62
- package/build/src/database/drivers/sqlite/table.js +125 -102
- package/build/src/database/drivers/structure-sql/utils.js +17 -14
- package/build/src/database/handler.js +10 -9
- package/build/src/database/initializer-from-require-context.js +87 -76
- package/build/src/database/migration/index.js +395 -332
- package/build/src/database/migrator/files-finder.js +50 -40
- package/build/src/database/migrator/types.js +30 -2
- package/build/src/database/migrator.js +526 -454
- package/build/src/database/pool/async-tracked-multi-connection.js +1147 -997
- package/build/src/database/pool/base-methods-forward.js +43 -40
- package/build/src/database/pool/base.js +343 -298
- package/build/src/database/pool/single-multi-use.js +110 -93
- package/build/src/database/query/alter-table-base.js +99 -84
- package/build/src/database/query/base.js +46 -39
- package/build/src/database/query/create-database-base.js +30 -25
- package/build/src/database/query/create-index-base.js +94 -75
- package/build/src/database/query/create-table-base.js +193 -151
- package/build/src/database/query/delete-base.js +16 -14
- package/build/src/database/query/drop-database-base.js +28 -23
- package/build/src/database/query/drop-table-base.js +53 -42
- package/build/src/database/query/from-base.js +33 -30
- package/build/src/database/query/from-plain.js +13 -11
- package/build/src/database/query/from-table.js +15 -13
- package/build/src/database/query/index.js +472 -410
- package/build/src/database/query/insert-base.js +164 -143
- package/build/src/database/query/join-base.js +40 -35
- package/build/src/database/query/join-object.js +153 -128
- package/build/src/database/query/join-plain.js +15 -13
- package/build/src/database/query/join-tracker.js +90 -76
- package/build/src/database/query/model-class-query.js +1370 -1134
- package/build/src/database/query/order-base.js +30 -27
- package/build/src/database/query/order-column.js +53 -44
- package/build/src/database/query/order-plain.js +24 -20
- package/build/src/database/query/preloader/belongs-to.js +258 -210
- package/build/src/database/query/preloader/ensure-model-class-initialized.js +9 -8
- package/build/src/database/query/preloader/has-many.js +301 -240
- package/build/src/database/query/preloader/has-one.js +117 -91
- package/build/src/database/query/preloader/selection.js +129 -117
- package/build/src/database/query/preloader.js +185 -160
- package/build/src/database/query/query-data.js +201 -157
- package/build/src/database/query/select-base.js +27 -25
- package/build/src/database/query/select-plain.js +15 -13
- package/build/src/database/query/select-table-and-column.js +25 -21
- package/build/src/database/query/update-base.js +38 -35
- package/build/src/database/query/upsert-base.js +100 -93
- package/build/src/database/query/where-base.js +35 -32
- package/build/src/database/query/where-combinator.d.ts.map +1 -1
- package/build/src/database/query/where-combinator.js +28 -26
- package/build/src/database/query/where-hash.js +68 -61
- package/build/src/database/query/where-model-class-hash.js +469 -414
- package/build/src/database/query/where-not.js +20 -18
- package/build/src/database/query/where-plain.js +17 -15
- package/build/src/database/query/with-count.js +159 -125
- package/build/src/database/query-parser/base-query-parser.js +37 -32
- package/build/src/database/query-parser/from-parser.js +45 -36
- package/build/src/database/query-parser/group-parser.js +50 -42
- package/build/src/database/query-parser/joins-parser.js +33 -28
- package/build/src/database/query-parser/limit-parser.js +70 -67
- package/build/src/database/query-parser/options.js +82 -75
- package/build/src/database/query-parser/order-parser.js +40 -36
- package/build/src/database/query-parser/select-parser.js +60 -49
- package/build/src/database/query-parser/where-parser.js +41 -36
- package/build/src/database/record/acts-as-list.d.ts.map +1 -1
- package/build/src/database/record/acts-as-list.js +273 -229
- package/build/src/database/record/attachments/download.js +45 -44
- package/build/src/database/record/attachments/handle.js +161 -141
- package/build/src/database/record/attachments/normalize-input.js +138 -128
- package/build/src/database/record/attachments/storage-drivers/filesystem.js +91 -77
- package/build/src/database/record/attachments/storage-drivers/native.js +121 -112
- package/build/src/database/record/attachments/storage-drivers/s3.js +208 -177
- package/build/src/database/record/attachments/store.d.ts +1 -1
- package/build/src/database/record/attachments/store.d.ts.map +1 -1
- package/build/src/database/record/attachments/store.js +540 -468
- package/build/src/database/record/index.d.ts +23 -15
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +3894 -3350
- package/build/src/database/record/instance-relationships/base.js +268 -234
- package/build/src/database/record/instance-relationships/belongs-to.js +73 -58
- package/build/src/database/record/instance-relationships/has-many.js +264 -225
- package/build/src/database/record/instance-relationships/has-one.js +105 -85
- package/build/src/database/record/record-not-found-error.js +2 -3
- package/build/src/database/record/relationships/base.d.ts +2 -2
- package/build/src/database/record/relationships/base.d.ts.map +1 -1
- package/build/src/database/record/relationships/base.js +167 -145
- package/build/src/database/record/relationships/belongs-to.js +51 -44
- package/build/src/database/record/relationships/has-many.js +40 -32
- package/build/src/database/record/relationships/has-one.js +40 -32
- package/build/src/database/record/state-machine.js +208 -156
- package/build/src/database/record/user-module.js +38 -32
- package/build/src/database/record/validators/base.js +24 -22
- package/build/src/database/record/validators/format.js +46 -36
- package/build/src/database/record/validators/presence.js +20 -18
- package/build/src/database/record/validators/uniqueness.js +117 -99
- package/build/src/database/table-data/index.js +231 -199
- package/build/src/database/table-data/table-column.js +382 -338
- package/build/src/database/table-data/table-foreign-key.js +66 -57
- package/build/src/database/table-data/table-index.js +36 -29
- package/build/src/database/table-data/table-reference.js +10 -10
- package/build/src/database/use-database.js +40 -32
- package/build/src/environment-handlers/base.js +544 -484
- package/build/src/environment-handlers/browser.js +294 -241
- package/build/src/environment-handlers/node/cli/commands/background-jobs-main.js +19 -16
- package/build/src/environment-handlers/node/cli/commands/background-jobs-runner.js +21 -18
- package/build/src/environment-handlers/node/cli/commands/background-jobs-worker.js +29 -22
- package/build/src/environment-handlers/node/cli/commands/beacon.js +19 -16
- package/build/src/environment-handlers/node/cli/commands/cli-command-context.js +15 -14
- package/build/src/environment-handlers/node/cli/commands/console.js +120 -99
- package/build/src/environment-handlers/node/cli/commands/db/schema/dump.js +39 -34
- package/build/src/environment-handlers/node/cli/commands/db/schema/load.js +63 -57
- package/build/src/environment-handlers/node/cli/commands/db/seed.js +63 -51
- package/build/src/environment-handlers/node/cli/commands/destroy/migration.js +40 -32
- package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/base-models.js +353 -298
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +844 -729
- package/build/src/environment-handlers/node/cli/commands/generate/migration.js +38 -34
- package/build/src/environment-handlers/node/cli/commands/generate/model.js +38 -34
- package/build/src/environment-handlers/node/cli/commands/init.js +61 -56
- package/build/src/environment-handlers/node/cli/commands/routes.js +59 -51
- package/build/src/environment-handlers/node/cli/commands/run-script.js +68 -54
- package/build/src/environment-handlers/node/cli/commands/runner.js +74 -56
- package/build/src/environment-handlers/node/cli/commands/server.js +106 -93
- package/build/src/environment-handlers/node/cli/commands/test.js +113 -97
- package/build/src/environment-handlers/node.js +874 -753
- package/build/src/error-logger.js +21 -22
- package/build/src/frontend-model-controller.d.ts +6 -6
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +3288 -2788
- package/build/src/frontend-model-resource/base-resource.d.ts +18 -17
- package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/base-resource.js +869 -759
- package/build/src/frontend-models/base.d.ts +19 -12
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +3602 -3114
- package/build/src/frontend-models/clear-pending-debounced-callback.js +8 -7
- package/build/src/frontend-models/event-hook-models.js +21 -16
- package/build/src/frontend-models/model-registry.js +11 -9
- package/build/src/frontend-models/outgoing-event-buffer.js +17 -10
- package/build/src/frontend-models/preloader.d.ts +6 -6
- package/build/src/frontend-models/preloader.d.ts.map +1 -1
- package/build/src/frontend-models/preloader.js +149 -131
- package/build/src/frontend-models/query.d.ts.map +1 -1
- package/build/src/frontend-models/query.js +1855 -1560
- package/build/src/frontend-models/resource-config-validation.js +37 -27
- package/build/src/frontend-models/resource-definition.js +288 -234
- package/build/src/frontend-models/transport-serialization.js +266 -203
- package/build/src/frontend-models/use-created-event.js +7 -5
- package/build/src/frontend-models/use-destroyed-event.js +93 -80
- package/build/src/frontend-models/use-model-class-event.js +91 -79
- package/build/src/frontend-models/use-updated-event.js +97 -84
- package/build/src/frontend-models/websocket-channel.js +441 -381
- package/build/src/frontend-models/websocket-publishers.js +173 -140
- package/build/src/http-client/header.js +14 -13
- package/build/src/http-client/index.js +132 -116
- package/build/src/http-client/request.js +87 -71
- package/build/src/http-client/response.js +140 -122
- package/build/src/http-client/websocket-client.js +17 -15
- package/build/src/http-server/client/index.js +465 -409
- package/build/src/http-server/client/params-to-object.js +135 -124
- package/build/src/http-server/client/request-buffer/form-data-part.js +132 -111
- package/build/src/http-server/client/request-buffer/header.js +16 -15
- package/build/src/http-server/client/request-buffer/index.js +506 -446
- package/build/src/http-server/client/request-parser.js +186 -163
- package/build/src/http-server/client/request-runner.js +259 -226
- package/build/src/http-server/client/request-timing.js +151 -132
- package/build/src/http-server/client/request.js +108 -96
- package/build/src/http-server/client/response.js +235 -213
- package/build/src/http-server/client/uploaded-file/memory-uploaded-file.js +29 -25
- package/build/src/http-server/client/uploaded-file/temporary-uploaded-file.js +29 -25
- package/build/src/http-server/client/uploaded-file/uploaded-file.js +33 -33
- package/build/src/http-server/client/websocket-request.js +137 -114
- package/build/src/http-server/client/websocket-session.js +1657 -1452
- package/build/src/http-server/cookie.js +236 -216
- package/build/src/http-server/development-reloader.js +221 -190
- package/build/src/http-server/index.js +525 -451
- package/build/src/http-server/remote-address.js +50 -38
- package/build/src/http-server/server-client.js +208 -181
- package/build/src/http-server/server-lock.js +167 -153
- package/build/src/http-server/websocket-channel-subscribers.js +93 -81
- package/build/src/http-server/websocket-channel.js +117 -104
- package/build/src/http-server/websocket-connection.js +104 -96
- package/build/src/http-server/websocket-event-log-store.js +404 -350
- package/build/src/http-server/websocket-events-host.js +164 -145
- package/build/src/http-server/websocket-events.js +47 -47
- package/build/src/http-server/worker-handler/channel-subscriber-dispatch.js +14 -13
- package/build/src/http-server/worker-handler/in-process.js +141 -123
- package/build/src/http-server/worker-handler/index.js +349 -313
- package/build/src/http-server/worker-handler/worker-script.js +5 -4
- package/build/src/http-server/worker-handler/worker-thread.js +269 -240
- package/build/src/initializer.js +36 -31
- package/build/src/jobs/mail-delivery.js +15 -13
- package/build/src/logger/base-logger.js +26 -24
- package/build/src/logger/console-logger.js +23 -21
- package/build/src/logger/file-logger.js +31 -29
- package/build/src/logger/outputs/array-output.js +42 -37
- package/build/src/logger/outputs/console-output.js +24 -20
- package/build/src/logger/outputs/file-output.js +48 -43
- package/build/src/logger/outputs/stdout-output.js +48 -39
- package/build/src/logger.js +394 -338
- package/build/src/mailer/backends/smtp.js +163 -134
- package/build/src/mailer/base.js +251 -211
- package/build/src/mailer/delivery.js +64 -56
- package/build/src/mailer/index.js +22 -4
- package/build/src/mailer.js +13 -4
- package/build/src/plugins/sqljs-wasm-route-controller.js +52 -42
- package/build/src/plugins/sqljs-wasm-route.js +38 -28
- package/build/src/record-payload-values.js +28 -25
- package/build/src/routes/app-routes.js +14 -12
- package/build/src/routes/base-route.js +130 -112
- package/build/src/routes/basic-route.js +102 -83
- package/build/src/routes/built-in/debug/controller.js +10 -10
- package/build/src/routes/built-in/errors/controller.js +5 -5
- package/build/src/routes/get-route.js +63 -50
- package/build/src/routes/hooks/frontend-model-command-route-hook.js +80 -66
- package/build/src/routes/index.js +43 -36
- package/build/src/routes/namespace-route.js +47 -38
- package/build/src/routes/plugin-routes.js +124 -107
- package/build/src/routes/post-route.js +62 -51
- package/build/src/routes/resolver.js +494 -422
- package/build/src/routes/resource-route.js +143 -124
- package/build/src/routes/root-route.js +8 -7
- package/build/src/testing/base-expect.js +14 -13
- package/build/src/testing/browser-frontend-model-event-hook-scenarios.js +405 -329
- package/build/src/testing/browser-test-app.js +29 -23
- package/build/src/testing/expect-to-change.js +50 -41
- package/build/src/testing/expect-utils.js +184 -139
- package/build/src/testing/expect.js +731 -638
- package/build/src/testing/request-client.js +85 -70
- package/build/src/testing/test-files-finder.js +339 -285
- package/build/src/testing/test-filter-parser.js +155 -124
- package/build/src/testing/test-runner.js +1020 -883
- package/build/src/testing/test-suite-splitter.js +142 -114
- package/build/src/testing/test.js +256 -216
- package/build/src/utils/backtrace-cleaner-node.js +69 -62
- package/build/src/utils/backtrace-cleaner.js +216 -188
- package/build/src/utils/ensure-error.js +7 -7
- package/build/src/utils/event-emitter.js +6 -4
- package/build/src/utils/file-exists.js +10 -9
- package/build/src/utils/format-value.js +76 -67
- package/build/src/utils/model-scope.js +31 -27
- package/build/src/utils/nest-callbacks.js +13 -10
- package/build/src/utils/plain-object.js +6 -5
- package/build/src/utils/ransack.d.ts.map +1 -1
- package/build/src/utils/ransack.js +563 -449
- package/build/src/utils/rest-args-error.js +6 -5
- package/build/src/utils/singularize-model-name.js +11 -9
- package/build/src/utils/split-sql-statements.js +79 -68
- package/build/src/utils/to-import-specifier.js +30 -24
- package/build/src/utils/with-tracked-stack-async-hooks.js +74 -60
- package/build/src/utils/with-tracked-stack.js +18 -14
- package/build/src/velocious-error.js +30 -27
- package/index.js +1 -0
- package/package.json +10 -4
- package/scripts/clean-build.js +8 -0
- package/scripts/ensure-bin-executable.js +13 -0
- package/scripts/run-tests.js +37 -0
- package/scripts/test-browser.js +486 -0
- package/src/application.js +229 -0
- package/src/authorization/ability.js +329 -0
- package/src/authorization/base-resource.js +143 -0
- package/src/background-jobs/client.js +50 -0
- package/src/background-jobs/cron-expression.js +277 -0
- package/src/background-jobs/forked-runner-child.js +86 -0
- package/src/background-jobs/job-record.js +13 -0
- package/src/background-jobs/job-registry.js +92 -0
- package/src/background-jobs/job-runner.js +107 -0
- package/src/background-jobs/job.js +77 -0
- package/src/background-jobs/json-socket.js +78 -0
- package/src/background-jobs/main.js +926 -0
- package/src/background-jobs/normalize-error.js +26 -0
- package/src/background-jobs/scheduler.js +274 -0
- package/src/background-jobs/socket-request.js +68 -0
- package/src/background-jobs/status-reporter.js +101 -0
- package/src/background-jobs/store.js +994 -0
- package/src/background-jobs/types.js +70 -0
- package/src/background-jobs/web/authorization.js +89 -0
- package/src/background-jobs/web/controller.js +280 -0
- package/src/background-jobs/web/index.js +57 -0
- package/src/background-jobs/web/path-matcher.js +74 -0
- package/src/background-jobs/web/registry.js +49 -0
- package/src/background-jobs/worker.js +683 -0
- package/src/beacon/client.js +330 -0
- package/src/beacon/in-process-broker.js +71 -0
- package/src/beacon/in-process-client.js +139 -0
- package/src/beacon/server.js +148 -0
- package/src/beacon/types.js +55 -0
- package/src/cli/base-command.js +67 -0
- package/src/cli/browser-cli.js +45 -0
- package/src/cli/commands/background-jobs-main.js +7 -0
- package/src/cli/commands/background-jobs-runner.js +7 -0
- package/src/cli/commands/background-jobs-worker.js +7 -0
- package/src/cli/commands/beacon.js +7 -0
- package/src/cli/commands/console.js +12 -0
- package/src/cli/commands/db/base-command.js +82 -0
- package/src/cli/commands/db/create.js +64 -0
- package/src/cli/commands/db/drop.js +75 -0
- package/src/cli/commands/db/migrate.js +17 -0
- package/src/cli/commands/db/reset.js +22 -0
- package/src/cli/commands/db/rollback.js +15 -0
- package/src/cli/commands/db/schema/dump.js +12 -0
- package/src/cli/commands/db/schema/load.js +12 -0
- package/src/cli/commands/db/seed.js +12 -0
- package/src/cli/commands/db/tenants/check.js +38 -0
- package/src/cli/commands/db/tenants/create.js +33 -0
- package/src/cli/commands/db/tenants/migrate.js +49 -0
- package/src/cli/commands/destroy/migration.js +7 -0
- package/src/cli/commands/generate/base-models.js +7 -0
- package/src/cli/commands/generate/frontend-models.js +12 -0
- package/src/cli/commands/generate/migration.js +7 -0
- package/src/cli/commands/generate/model.js +7 -0
- package/src/cli/commands/init.js +11 -0
- package/src/cli/commands/routes.js +7 -0
- package/src/cli/commands/run-script.js +12 -0
- package/src/cli/commands/runner.js +12 -0
- package/src/cli/commands/server.js +7 -0
- package/src/cli/commands/test.js +9 -0
- package/src/cli/index.js +152 -0
- package/src/cli/tenant-database-command-helper.js +198 -0
- package/src/cli/use-browser-cli.js +30 -0
- package/src/configuration-resolver.js +65 -0
- package/src/configuration-types.js +429 -0
- package/src/configuration.js +2590 -0
- package/src/controller.js +421 -0
- package/src/current-configuration.js +31 -0
- package/src/current.js +80 -0
- package/src/database/annotations-async-hooks.js +47 -0
- package/src/database/annotations.js +40 -0
- package/src/database/drivers/base-column.js +182 -0
- package/src/database/drivers/base-columns-index.js +81 -0
- package/src/database/drivers/base-foreign-key.js +104 -0
- package/src/database/drivers/base-table.js +156 -0
- package/src/database/drivers/base.js +1609 -0
- package/src/database/drivers/mssql/column.js +74 -0
- package/src/database/drivers/mssql/columns-index.js +6 -0
- package/src/database/drivers/mssql/connect-connection.js +16 -0
- package/src/database/drivers/mssql/foreign-key.js +12 -0
- package/src/database/drivers/mssql/index.js +590 -0
- package/src/database/drivers/mssql/options.js +79 -0
- package/src/database/drivers/mssql/query-parser.js +6 -0
- package/src/database/drivers/mssql/sql/alter-table.js +4 -0
- package/src/database/drivers/mssql/sql/create-database.js +36 -0
- package/src/database/drivers/mssql/sql/create-index.js +4 -0
- package/src/database/drivers/mssql/sql/create-table.js +4 -0
- package/src/database/drivers/mssql/sql/delete.js +19 -0
- package/src/database/drivers/mssql/sql/drop-database.js +36 -0
- package/src/database/drivers/mssql/sql/drop-table.js +4 -0
- package/src/database/drivers/mssql/sql/insert.js +4 -0
- package/src/database/drivers/mssql/sql/update.js +31 -0
- package/src/database/drivers/mssql/sql/upsert.js +23 -0
- package/src/database/drivers/mssql/structure-sql.js +120 -0
- package/src/database/drivers/mssql/table.js +145 -0
- package/src/database/drivers/mysql/column.js +112 -0
- package/src/database/drivers/mysql/columns-index.js +22 -0
- package/src/database/drivers/mysql/foreign-key.js +12 -0
- package/src/database/drivers/mysql/index.js +473 -0
- package/src/database/drivers/mysql/options.js +34 -0
- package/src/database/drivers/mysql/query-parser.js +6 -0
- package/src/database/drivers/mysql/query.js +37 -0
- package/src/database/drivers/mysql/sql/alter-table.js +6 -0
- package/src/database/drivers/mysql/sql/create-database.js +39 -0
- package/src/database/drivers/mysql/sql/create-index.js +6 -0
- package/src/database/drivers/mysql/sql/create-table.js +6 -0
- package/src/database/drivers/mysql/sql/delete.js +21 -0
- package/src/database/drivers/mysql/sql/drop-database.js +6 -0
- package/src/database/drivers/mysql/sql/drop-table.js +6 -0
- package/src/database/drivers/mysql/sql/insert.js +6 -0
- package/src/database/drivers/mysql/sql/update.js +33 -0
- package/src/database/drivers/mysql/sql/upsert.js +13 -0
- package/src/database/drivers/mysql/structure-sql.js +93 -0
- package/src/database/drivers/mysql/table.js +121 -0
- package/src/database/drivers/pgsql/column.js +90 -0
- package/src/database/drivers/pgsql/columns-index.js +6 -0
- package/src/database/drivers/pgsql/foreign-key.js +12 -0
- package/src/database/drivers/pgsql/index.js +441 -0
- package/src/database/drivers/pgsql/options.js +32 -0
- package/src/database/drivers/pgsql/query-parser.js +6 -0
- package/src/database/drivers/pgsql/sql/alter-table.js +6 -0
- package/src/database/drivers/pgsql/sql/create-database.js +38 -0
- package/src/database/drivers/pgsql/sql/create-index.js +6 -0
- package/src/database/drivers/pgsql/sql/create-table.js +6 -0
- package/src/database/drivers/pgsql/sql/delete.js +21 -0
- package/src/database/drivers/pgsql/sql/drop-database.js +6 -0
- package/src/database/drivers/pgsql/sql/drop-table.js +6 -0
- package/src/database/drivers/pgsql/sql/insert.js +6 -0
- package/src/database/drivers/pgsql/sql/update.js +33 -0
- package/src/database/drivers/pgsql/sql/upsert.js +14 -0
- package/src/database/drivers/pgsql/structure-sql.js +126 -0
- package/src/database/drivers/pgsql/table.js +135 -0
- package/src/database/drivers/sqlite/base.js +509 -0
- package/src/database/drivers/sqlite/column.js +75 -0
- package/src/database/drivers/sqlite/columns-index.js +30 -0
- package/src/database/drivers/sqlite/connection-sql-js.js +46 -0
- package/src/database/drivers/sqlite/foreign-key.js +24 -0
- package/src/database/drivers/sqlite/index.js +394 -0
- package/src/database/drivers/sqlite/index.native.js +72 -0
- package/src/database/drivers/sqlite/index.web.js +99 -0
- package/src/database/drivers/sqlite/options.js +32 -0
- package/src/database/drivers/sqlite/query-parser.js +6 -0
- package/src/database/drivers/sqlite/query.js +35 -0
- package/src/database/drivers/sqlite/query.native.js +35 -0
- package/src/database/drivers/sqlite/query.web.js +49 -0
- package/src/database/drivers/sqlite/sql/alter-table.js +187 -0
- package/src/database/drivers/sqlite/sql/create-index.js +6 -0
- package/src/database/drivers/sqlite/sql/create-table.js +6 -0
- package/src/database/drivers/sqlite/sql/delete.js +26 -0
- package/src/database/drivers/sqlite/sql/drop-table.js +6 -0
- package/src/database/drivers/sqlite/sql/insert.js +6 -0
- package/src/database/drivers/sqlite/sql/update.js +33 -0
- package/src/database/drivers/sqlite/sql/upsert.js +14 -0
- package/src/database/drivers/sqlite/structure-sql.js +56 -0
- package/src/database/drivers/sqlite/table-rebuilder.js +96 -0
- package/src/database/drivers/sqlite/table.js +131 -0
- package/src/database/drivers/structure-sql/utils.js +35 -0
- package/src/database/handler.js +13 -0
- package/src/database/initializer-from-require-context.js +101 -0
- package/src/database/migration/index.js +438 -0
- package/src/database/migrator/files-finder.js +55 -0
- package/src/database/migrator/types.js +31 -0
- package/src/database/migrator.js +557 -0
- package/src/database/pool/async-tracked-multi-connection.js +1164 -0
- package/src/database/pool/base-methods-forward.js +52 -0
- package/src/database/pool/base.js +380 -0
- package/src/database/pool/single-multi-use.js +118 -0
- package/src/database/query/alter-table-base.js +104 -0
- package/src/database/query/base.js +49 -0
- package/src/database/query/create-database-base.js +42 -0
- package/src/database/query/create-index-base.js +117 -0
- package/src/database/query/create-table-base.js +205 -0
- package/src/database/query/delete-base.js +19 -0
- package/src/database/query/drop-database-base.js +38 -0
- package/src/database/query/drop-table-base.js +58 -0
- package/src/database/query/from-base.js +36 -0
- package/src/database/query/from-plain.js +16 -0
- package/src/database/query/from-table.js +18 -0
- package/src/database/query/index.js +533 -0
- package/src/database/query/insert-base.js +172 -0
- package/src/database/query/join-base.js +43 -0
- package/src/database/query/join-object.js +167 -0
- package/src/database/query/join-plain.js +18 -0
- package/src/database/query/join-tracker.js +93 -0
- package/src/database/query/model-class-query.js +1577 -0
- package/src/database/query/order-base.js +33 -0
- package/src/database/query/order-column.js +77 -0
- package/src/database/query/order-plain.js +28 -0
- package/src/database/query/preloader/belongs-to.js +267 -0
- package/src/database/query/preloader/ensure-model-class-initialized.js +18 -0
- package/src/database/query/preloader/has-many.js +316 -0
- package/src/database/query/preloader/has-one.js +123 -0
- package/src/database/query/preloader/selection.js +152 -0
- package/src/database/query/preloader.js +201 -0
- package/src/database/query/query-data.js +305 -0
- package/src/database/query/select-base.js +30 -0
- package/src/database/query/select-plain.js +18 -0
- package/src/database/query/select-table-and-column.js +28 -0
- package/src/database/query/update-base.js +41 -0
- package/src/database/query/upsert-base.js +103 -0
- package/src/database/query/where-base.js +38 -0
- package/src/database/query/where-combinator.js +31 -0
- package/src/database/query/where-hash.js +77 -0
- package/src/database/query/where-model-class-hash.js +505 -0
- package/src/database/query/where-not.js +23 -0
- package/src/database/query/where-plain.js +20 -0
- package/src/database/query/with-count.js +219 -0
- package/src/database/query-parser/base-query-parser.js +40 -0
- package/src/database/query-parser/from-parser.js +49 -0
- package/src/database/query-parser/group-parser.js +55 -0
- package/src/database/query-parser/joins-parser.js +37 -0
- package/src/database/query-parser/limit-parser.js +77 -0
- package/src/database/query-parser/options.js +94 -0
- package/src/database/query-parser/order-parser.js +45 -0
- package/src/database/query-parser/select-parser.js +67 -0
- package/src/database/query-parser/where-parser.js +46 -0
- package/src/database/record/acts-as-list.js +374 -0
- package/src/database/record/attachments/download.js +49 -0
- package/src/database/record/attachments/handle.js +188 -0
- package/src/database/record/attachments/normalize-input.js +213 -0
- package/src/database/record/attachments/storage-drivers/filesystem.js +114 -0
- package/src/database/record/attachments/storage-drivers/native.js +146 -0
- package/src/database/record/attachments/storage-drivers/s3.js +245 -0
- package/src/database/record/attachments/store.js +591 -0
- package/src/database/record/index.js +3970 -0
- package/src/database/record/instance-relationships/base.js +289 -0
- package/src/database/record/instance-relationships/belongs-to.js +84 -0
- package/src/database/record/instance-relationships/has-many.js +284 -0
- package/src/database/record/instance-relationships/has-one.js +117 -0
- package/src/database/record/record-not-found-error.js +3 -0
- package/src/database/record/relationships/base.js +195 -0
- package/src/database/record/relationships/belongs-to.js +57 -0
- package/src/database/record/relationships/has-many.js +46 -0
- package/src/database/record/relationships/has-one.js +46 -0
- package/src/database/record/state-machine.js +278 -0
- package/src/database/record/user-module.js +43 -0
- package/src/database/record/validators/base.js +27 -0
- package/src/database/record/validators/format.js +50 -0
- package/src/database/record/validators/presence.js +24 -0
- package/src/database/record/validators/uniqueness.js +124 -0
- package/src/database/table-data/index.js +241 -0
- package/src/database/table-data/table-column.js +416 -0
- package/src/database/table-data/table-foreign-key.js +69 -0
- package/src/database/table-data/table-index.js +46 -0
- package/src/database/table-data/table-reference.js +13 -0
- package/src/database/use-database.js +48 -0
- package/src/environment-handlers/base.js +561 -0
- package/src/environment-handlers/browser.js +338 -0
- package/src/environment-handlers/node/cli/commands/background-jobs-main.js +21 -0
- package/src/environment-handlers/node/cli/commands/background-jobs-runner.js +24 -0
- package/src/environment-handlers/node/cli/commands/background-jobs-worker.js +47 -0
- package/src/environment-handlers/node/cli/commands/beacon.js +21 -0
- package/src/environment-handlers/node/cli/commands/cli-command-context.js +31 -0
- package/src/environment-handlers/node/cli/commands/console.js +149 -0
- package/src/environment-handlers/node/cli/commands/db/schema/dump.js +43 -0
- package/src/environment-handlers/node/cli/commands/db/schema/load.js +69 -0
- package/src/environment-handlers/node/cli/commands/db/seed.js +79 -0
- package/src/environment-handlers/node/cli/commands/destroy/migration.js +47 -0
- package/src/environment-handlers/node/cli/commands/generate/base-models.js +367 -0
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +872 -0
- package/src/environment-handlers/node/cli/commands/generate/migration.js +45 -0
- package/src/environment-handlers/node/cli/commands/generate/model.js +45 -0
- package/src/environment-handlers/node/cli/commands/init.js +68 -0
- package/src/environment-handlers/node/cli/commands/routes.js +63 -0
- package/src/environment-handlers/node/cli/commands/run-script.js +85 -0
- package/src/environment-handlers/node/cli/commands/runner.js +84 -0
- package/src/environment-handlers/node/cli/commands/server.js +151 -0
- package/src/environment-handlers/node/cli/commands/test.js +118 -0
- package/src/environment-handlers/node.js +887 -0
- package/src/error-logger.js +30 -0
- package/src/frontend-model-controller.js +3491 -0
- package/src/frontend-model-resource/base-resource.js +935 -0
- package/src/frontend-models/base.js +4004 -0
- package/src/frontend-models/clear-pending-debounced-callback.js +16 -0
- package/src/frontend-models/event-hook-models.js +49 -0
- package/src/frontend-models/model-registry.js +28 -0
- package/src/frontend-models/outgoing-event-buffer.js +51 -0
- package/src/frontend-models/preloader.js +169 -0
- package/src/frontend-models/query.js +2245 -0
- package/src/frontend-models/resource-config-validation.js +56 -0
- package/src/frontend-models/resource-definition.js +399 -0
- package/src/frontend-models/transport-serialization.js +369 -0
- package/src/frontend-models/use-created-event.js +21 -0
- package/src/frontend-models/use-destroyed-event.js +148 -0
- package/src/frontend-models/use-model-class-event.js +164 -0
- package/src/frontend-models/use-updated-event.js +152 -0
- package/src/frontend-models/websocket-channel.js +494 -0
- package/src/frontend-models/websocket-publishers.js +224 -0
- package/src/http-client/header.js +17 -0
- package/src/http-client/index.js +139 -0
- package/src/http-client/request.js +94 -0
- package/src/http-client/response.js +151 -0
- package/src/http-client/websocket-client.js +27 -0
- package/src/http-server/client/index.js +507 -0
- package/src/http-server/client/params-to-object.js +152 -0
- package/src/http-server/client/request-buffer/form-data-part.js +139 -0
- package/src/http-server/client/request-buffer/header.js +19 -0
- package/src/http-server/client/request-buffer/index.js +535 -0
- package/src/http-server/client/request-parser.js +195 -0
- package/src/http-server/client/request-runner.js +321 -0
- package/src/http-server/client/request-timing.js +171 -0
- package/src/http-server/client/request.js +114 -0
- package/src/http-server/client/response.js +251 -0
- package/src/http-server/client/uploaded-file/memory-uploaded-file.js +32 -0
- package/src/http-server/client/uploaded-file/temporary-uploaded-file.js +32 -0
- package/src/http-server/client/uploaded-file/uploaded-file.js +36 -0
- package/src/http-server/client/websocket-request.js +147 -0
- package/src/http-server/client/websocket-session.js +1755 -0
- package/src/http-server/cookie.js +245 -0
- package/src/http-server/development-reloader.js +240 -0
- package/src/http-server/index.js +561 -0
- package/src/http-server/remote-address.js +77 -0
- package/src/http-server/server-client.js +222 -0
- package/src/http-server/server-lock.js +178 -0
- package/src/http-server/websocket-channel-subscribers.js +110 -0
- package/src/http-server/websocket-channel.js +137 -0
- package/src/http-server/websocket-connection.js +118 -0
- package/src/http-server/websocket-event-log-store.js +433 -0
- package/src/http-server/websocket-events-host.js +170 -0
- package/src/http-server/websocket-events.js +50 -0
- package/src/http-server/worker-handler/channel-subscriber-dispatch.js +28 -0
- package/src/http-server/worker-handler/in-process.js +155 -0
- package/src/http-server/worker-handler/index.js +370 -0
- package/src/http-server/worker-handler/worker-script.js +6 -0
- package/src/http-server/worker-handler/worker-thread.js +286 -0
- package/src/initializer.js +39 -0
- package/src/jobs/.gitkeep +1 -0
- package/src/jobs/mail-delivery.js +22 -0
- package/src/logger/base-logger.js +34 -0
- package/src/logger/console-logger.js +28 -0
- package/src/logger/file-logger.js +36 -0
- package/src/logger/outputs/array-output.js +50 -0
- package/src/logger/outputs/console-output.js +32 -0
- package/src/logger/outputs/file-output.js +55 -0
- package/src/logger/outputs/stdout-output.js +64 -0
- package/src/logger.js +507 -0
- package/src/mailer/backends/smtp.js +197 -0
- package/src/mailer/base.js +337 -0
- package/src/mailer/delivery.js +70 -0
- package/src/mailer/index.js +24 -0
- package/src/mailer.js +15 -0
- package/src/plugins/sqljs-wasm-route-controller.js +70 -0
- package/src/plugins/sqljs-wasm-route.js +71 -0
- package/src/record-payload-values.js +83 -0
- package/src/routes/app-routes.js +17 -0
- package/src/routes/base-route.js +133 -0
- package/src/routes/basic-route.js +109 -0
- package/src/routes/built-in/debug/controller.js +12 -0
- package/src/routes/built-in/errors/controller.js +7 -0
- package/src/routes/built-in/errors/not-found.ejs +1 -0
- package/src/routes/get-route.js +75 -0
- package/src/routes/hooks/frontend-model-command-route-hook.js +100 -0
- package/src/routes/index.js +50 -0
- package/src/routes/namespace-route.js +51 -0
- package/src/routes/plugin-routes.js +141 -0
- package/src/routes/post-route.js +74 -0
- package/src/routes/resolver.js +535 -0
- package/src/routes/resource-route.js +154 -0
- package/src/routes/root-route.js +11 -0
- package/src/templates/configuration.js +61 -0
- package/src/templates/generate-migration.js +11 -0
- package/src/templates/generate-model.js +6 -0
- package/src/templates/routes.js +11 -0
- package/src/testing/base-expect.js +17 -0
- package/src/testing/browser-frontend-model-event-hook-scenarios.js +520 -0
- package/src/testing/browser-test-app.js +32 -0
- package/src/testing/expect-to-change.js +55 -0
- package/src/testing/expect-utils.js +269 -0
- package/src/testing/expect.js +763 -0
- package/src/testing/request-client.js +90 -0
- package/src/testing/test-files-finder.js +364 -0
- package/src/testing/test-filter-parser.js +198 -0
- package/src/testing/test-runner.js +1168 -0
- package/src/testing/test-suite-splitter.js +177 -0
- package/src/testing/test.js +370 -0
- package/src/types/external-modules.d.ts +57 -0
- package/src/utils/backtrace-cleaner-node.js +87 -0
- package/src/utils/backtrace-cleaner.js +266 -0
- package/src/utils/ensure-error.js +15 -0
- package/src/utils/event-emitter.js +8 -0
- package/src/utils/file-exists.js +18 -0
- package/src/utils/format-value.js +101 -0
- package/src/utils/model-scope.js +56 -0
- package/src/utils/nest-callbacks.js +22 -0
- package/src/utils/plain-object.js +14 -0
- package/src/utils/ransack.js +859 -0
- package/src/utils/rest-args-error.js +14 -0
- package/src/utils/singularize-model-name.js +18 -0
- package/src/utils/split-sql-statements.js +88 -0
- package/src/utils/to-import-specifier.js +53 -0
- package/src/utils/with-tracked-stack-async-hooks.js +103 -0
- package/src/utils/with-tracked-stack.js +38 -0
- package/src/velocious-error.js +34 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,1755 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {randomUUID} from "node:crypto"
|
|
4
|
+
|
|
5
|
+
import EventEmitter from "../../utils/event-emitter.js"
|
|
6
|
+
import Logger from "../../logger.js"
|
|
7
|
+
import RequestRunner from "./request-runner.js"
|
|
8
|
+
import WebsocketRequest from "./websocket-request.js"
|
|
9
|
+
import WebsocketChannel from "../websocket-channel.js"
|
|
10
|
+
import {websocketEventLogStoreForConfiguration} from "../websocket-event-log-store.js"
|
|
11
|
+
|
|
12
|
+
const WEBSOCKET_FINAL_FRAME = 0x80
|
|
13
|
+
const WEBSOCKET_OPCODE_CONTINUATION = 0x0
|
|
14
|
+
const WEBSOCKET_OPCODE_TEXT = 0x1
|
|
15
|
+
const WEBSOCKET_OPCODE_BINARY = 0x2
|
|
16
|
+
const WEBSOCKET_OPCODE_CLOSE = 0x8
|
|
17
|
+
const WEBSOCKET_OPCODE_PING = 0x9
|
|
18
|
+
const WEBSOCKET_OPCODE_PONG = 0xA
|
|
19
|
+
|
|
20
|
+
/** Cap on the paused outbound queue; oldest frames drop on overflow. */
|
|
21
|
+
const WEBSOCKET_PAUSED_QUEUE_CAP = 1000
|
|
22
|
+
|
|
23
|
+
/** Cap on total bytes buffered for a single fragmented message. */
|
|
24
|
+
const WEBSOCKET_MAX_FRAGMENTED_MESSAGE_BYTES = 16 * 1024 * 1024
|
|
25
|
+
|
|
26
|
+
/** Cap on fragment count for a single fragmented message. */
|
|
27
|
+
const WEBSOCKET_MAX_FRAGMENTED_MESSAGE_FRAGMENTS = 1024
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Defines this typedef.
|
|
31
|
+
* @typedef {{type: "subscribe", channel: string, lastEventId?: string, params?: Record<string, ?>} | {type: "metadata", data?: Record<string, ?>} | {type?: "request", body?: ?, headers?: Record<string, ?>, id?: string | number | null, method: string, path: string} | Record<string, ?>} WebsocketSessionMessage
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Runs subscribe message.
|
|
36
|
+
* @param {WebsocketSessionMessage} message - Raw websocket message.
|
|
37
|
+
* @returns {{type: "subscribe", channel: string, lastEventId?: string, params?: Record<string, ?>} | null} - Subscribe message when matched.
|
|
38
|
+
*/
|
|
39
|
+
function subscribeMessage(message) {
|
|
40
|
+
return message.type === "subscribe"
|
|
41
|
+
? /**
|
|
42
|
+
* Narrows the runtime value to the documented type.
|
|
43
|
+
@type {{type: "subscribe", channel: string, lastEventId?: string, params?: Record<string, ?>}} */ (message)
|
|
44
|
+
: null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Runs request message.
|
|
49
|
+
* @param {WebsocketSessionMessage} message - Raw websocket message.
|
|
50
|
+
* @returns {{type?: "request", body?: ?, headers?: Record<string, ?>, id?: string | number | null, method: string, path: string} | null} - Request message when matched.
|
|
51
|
+
*/
|
|
52
|
+
function requestMessage(message) {
|
|
53
|
+
if (message.type && message.type !== "request") return null
|
|
54
|
+
|
|
55
|
+
return /** Narrows the runtime value to the documented type. @type {{type?: "request", body?: ?, headers?: Record<string, ?>, id?: string | number | null, method: string, path: string}} */ (message)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Compares two identity values from `getWebsocketSessionIdentityResolver`.
|
|
60
|
+
* Nullish values compare equal to each other but not to a real identity.
|
|
61
|
+
* Plain objects are compared via JSON round-trip so apps can return a
|
|
62
|
+
* `{userId, tenantId}`-style object without building their own equality.
|
|
63
|
+
* @param {?} a - Paused-time identity.
|
|
64
|
+
* @param {?} b - Resume-time identity.
|
|
65
|
+
* @returns {boolean} - True when the two identities are considered the same caller.
|
|
66
|
+
*/
|
|
67
|
+
function identitiesMatch(a, b) {
|
|
68
|
+
if (a === b) return true
|
|
69
|
+
if (a == null || b == null) return false
|
|
70
|
+
if (typeof a !== "object" || typeof b !== "object") return false
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
return JSON.stringify(a) === JSON.stringify(b)
|
|
74
|
+
} catch {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default class VelociousHttpServerClientWebsocketSession {
|
|
80
|
+
events = new EventEmitter()
|
|
81
|
+
subscriptions = new Set()
|
|
82
|
+
channels = new Set()
|
|
83
|
+
subscriptionHandlers = new Map()
|
|
84
|
+
handlerSubscriptions = new Map()
|
|
85
|
+
channelTenants = new Map()
|
|
86
|
+
channelReplayStates = new Map()
|
|
87
|
+
/**
|
|
88
|
+
* Message queue.
|
|
89
|
+
@type {WebsocketSessionMessage[]} */
|
|
90
|
+
messageQueue = []
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Runs constructor.
|
|
94
|
+
* @param {object} args - Options object.
|
|
95
|
+
* @param {import("../../configuration.js").default} args.configuration - Configuration instance.
|
|
96
|
+
* @param {import("./index.js").default} args.client - Client instance.
|
|
97
|
+
* @param {import("./request.js").default | import("./websocket-request.js").default} [args.upgradeRequest] - Initial websocket upgrade request.
|
|
98
|
+
* @param {import("../../configuration-types.js").WebsocketMessageHandler} [args.messageHandler] - Optional raw message handler.
|
|
99
|
+
* @param {Promise<import("../../configuration-types.js").WebsocketMessageHandler | void>} [args.messageHandlerPromise] - Optional raw message handler promise.
|
|
100
|
+
*/
|
|
101
|
+
constructor({client, configuration, upgradeRequest, messageHandler, messageHandlerPromise}) {
|
|
102
|
+
this.buffer = Buffer.alloc(0)
|
|
103
|
+
this.client = client
|
|
104
|
+
this.configuration = configuration
|
|
105
|
+
this.upgradeRequest = upgradeRequest
|
|
106
|
+
this.messageHandler = messageHandler
|
|
107
|
+
this.messageHandlerPromise = messageHandlerPromise
|
|
108
|
+
this.pendingMessageHandler = Boolean(messageHandlerPromise)
|
|
109
|
+
this.logger = new Logger(this)
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Narrows the runtime value to the documented type.
|
|
113
|
+
@type {Record<string, ?>} */
|
|
114
|
+
this._metadata = {}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Long-lived per-session state bag. Stable across reconnects once
|
|
118
|
+
* grace-period resumption lands in Phase 2; today it just lives
|
|
119
|
+
* for the duration of the underlying socket.
|
|
120
|
+
* @type {Record<string, ?>}
|
|
121
|
+
*/
|
|
122
|
+
this.data = {}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Narrows the runtime value to the documented type.
|
|
126
|
+
@type {Map<string, import("../websocket-connection.js").default>} */
|
|
127
|
+
this._connections = new Map()
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Narrows the runtime value to the documented type.
|
|
131
|
+
@type {Map<string, {channelType: string, subscription: import("../websocket-channel.js").default}>} */
|
|
132
|
+
this._channelSubscriptions = new Map()
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Unique id assigned to this session on first connect. Sent to the
|
|
136
|
+
* client via `session-established`; the client echoes it back via
|
|
137
|
+
* `session-resume` after a WS drop to reattach to this session
|
|
138
|
+
* within the grace period.
|
|
139
|
+
* @type {string}
|
|
140
|
+
*/
|
|
141
|
+
this.sessionId = randomUUID()
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Narrows the runtime value to the documented type.
|
|
145
|
+
* @type {boolean} - true after `_handleClose` pauses instead of tearing down.
|
|
146
|
+
*/
|
|
147
|
+
this._paused = false
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Narrows the runtime value to the documented type.
|
|
151
|
+
* @type {Array<?>} - frames produced while paused; flushed on resume.
|
|
152
|
+
*/
|
|
153
|
+
this._outboundQueue = []
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Narrows the runtime value to the documented type.
|
|
157
|
+
@type {import("./index.js").default | null} */
|
|
158
|
+
this.socket = null
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Tail of a per-session promise chain that serializes message
|
|
162
|
+
* handling. Prevents races where message B reads `session.data`
|
|
163
|
+
* before message A's handler finishes writing it (e.g. a
|
|
164
|
+
* connection-message setting the locale vs. a subsequent request
|
|
165
|
+
* whose aroundRequest wrapper reads it).
|
|
166
|
+
* @type {Promise<void>}
|
|
167
|
+
*/
|
|
168
|
+
this._messageChain = Promise.resolve()
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Promise that resolves to the auth identity captured at pause
|
|
172
|
+
* time by `getWebsocketSessionIdentityResolver`. Awaited at resume
|
|
173
|
+
* time to compare against the fresh caller's identity. Undefined
|
|
174
|
+
* on a live (non-paused) session.
|
|
175
|
+
* @type {Promise<?> | undefined}
|
|
176
|
+
*/
|
|
177
|
+
this._resumeIdentityPromise = undefined
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Accumulates payloads for a fragmented websocket message per
|
|
181
|
+
* RFC 6455. Non-null while mid-fragment; cleared when the frame
|
|
182
|
+
* with FIN=1 completes and the message is dispatched.
|
|
183
|
+
* @type {Buffer[] | null}
|
|
184
|
+
*/
|
|
185
|
+
this._fragmentedPayloads = null
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Opcode (TEXT/BINARY) captured from the first frame of a
|
|
189
|
+
* fragmented message. Continuation frames (opcode 0) inherit it
|
|
190
|
+
* at reassembly time.
|
|
191
|
+
* @type {number | null}
|
|
192
|
+
*/
|
|
193
|
+
this._fragmentedOpcode = null
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Running byte total for `_fragmentedPayloads`. Used to enforce
|
|
197
|
+
* `WEBSOCKET_MAX_FRAGMENTED_MESSAGE_BYTES` so a peer cannot
|
|
198
|
+
* exhaust memory by streaming non-final fragments indefinitely.
|
|
199
|
+
* @type {number}
|
|
200
|
+
*/
|
|
201
|
+
this._fragmentedBytes = 0
|
|
202
|
+
|
|
203
|
+
this.configuration._websocketSessions.add(this)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Sends the client its sessionId + grace window. Called by
|
|
208
|
+
* `VelociousHttpServerClient` after the WS upgrade completes.
|
|
209
|
+
* @returns {void}
|
|
210
|
+
*/
|
|
211
|
+
sendSessionEstablished() {
|
|
212
|
+
this.sendJson({
|
|
213
|
+
type: "session-established",
|
|
214
|
+
sessionId: this.sessionId,
|
|
215
|
+
graceSeconds: this.configuration.getWebsocketSessionGraceSeconds?.() || 300
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Removes a closed connection from the session registry. Called by
|
|
221
|
+
* `VelociousWebsocketConnection.close()` after it sends the final
|
|
222
|
+
* `connection-closed` frame.
|
|
223
|
+
* @param {string} connectionId
|
|
224
|
+
* @returns {void}
|
|
225
|
+
*/
|
|
226
|
+
_removeConnection(connectionId) {
|
|
227
|
+
this._connections.delete(connectionId)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Runs get metadata.
|
|
232
|
+
* @returns {Record<string, ?>} - Client-provided metadata (defensive copy).
|
|
233
|
+
*/
|
|
234
|
+
getMetadata() {
|
|
235
|
+
return {...this._metadata}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Runs is paused.
|
|
240
|
+
* @returns {boolean} - true while the session is in the paused/grace registry.
|
|
241
|
+
*/
|
|
242
|
+
isPaused() {
|
|
243
|
+
return this._paused
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Runs add subscription.
|
|
248
|
+
* @param {string} channel - Channel name.
|
|
249
|
+
* @returns {void} - No return value.
|
|
250
|
+
*/
|
|
251
|
+
addSubscription(channel) {
|
|
252
|
+
this.subscriptions.add(channel)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
destroy() {
|
|
256
|
+
this.configuration._websocketSessions.delete(this)
|
|
257
|
+
this._paused = false
|
|
258
|
+
void this._teardownChannel()
|
|
259
|
+
void this._teardownConnections("session_destroyed")
|
|
260
|
+
void this._teardownChannelSubscriptions()
|
|
261
|
+
this.events.removeAllListeners()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Runs has subscription.
|
|
266
|
+
* @param {string} channel - Channel name.
|
|
267
|
+
* @returns {boolean} - Whether it has subscription.
|
|
268
|
+
*/
|
|
269
|
+
hasSubscription(channel) {
|
|
270
|
+
return this.subscriptions.has(channel)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Runs on data.
|
|
275
|
+
* @param {Buffer} data - Data payload.
|
|
276
|
+
* @returns {void} - No return value.
|
|
277
|
+
*/
|
|
278
|
+
onData(data) {
|
|
279
|
+
this.buffer = Buffer.concat([this.buffer, data])
|
|
280
|
+
this._processBuffer()
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Runs send event.
|
|
285
|
+
* @param {string} channel - Channel name.
|
|
286
|
+
* @param {?} payload - Payload data.
|
|
287
|
+
* @param {{createdAt?: string, eventId?: string, replayed?: boolean, sequence?: number}} [options] - Event metadata.
|
|
288
|
+
* @returns {Promise<void>} - Resolves when complete.
|
|
289
|
+
*/
|
|
290
|
+
async sendEvent(channel, payload, options = {}) {
|
|
291
|
+
const channelHandlers = this.subscriptionHandlers.get(channel)
|
|
292
|
+
const hasChannelHandlers = Boolean(channelHandlers && channelHandlers.size > 0)
|
|
293
|
+
const replayState = this.channelReplayStates.get(channel)
|
|
294
|
+
|
|
295
|
+
if (replayState?.replaying && !options.replayed) {
|
|
296
|
+
replayState.buffered = true
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!this.hasSubscription(channel) && !hasChannelHandlers) return
|
|
301
|
+
|
|
302
|
+
if (hasChannelHandlers) {
|
|
303
|
+
await Promise.all(Array.from(channelHandlers).map(async (handler) => {
|
|
304
|
+
const tenant = this.channelTenants.get(handler)
|
|
305
|
+
|
|
306
|
+
await this.configuration.runWithTenant(tenant, async () => {
|
|
307
|
+
await this._withConnections(async () => {
|
|
308
|
+
await handler.receivedBroadcast({
|
|
309
|
+
channel,
|
|
310
|
+
createdAt: options.createdAt,
|
|
311
|
+
eventId: options.eventId,
|
|
312
|
+
payload,
|
|
313
|
+
replayed: options.replayed,
|
|
314
|
+
sequence: options.sequence
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
}))
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
this.sendJson({
|
|
323
|
+
channel,
|
|
324
|
+
createdAt: options.createdAt,
|
|
325
|
+
eventId: options.eventId,
|
|
326
|
+
payload,
|
|
327
|
+
replayed: options.replayed,
|
|
328
|
+
sequence: options.sequence,
|
|
329
|
+
type: "event"
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Runs initialize channel.
|
|
335
|
+
* @returns {Promise<void>} - Resolves when complete.
|
|
336
|
+
*/
|
|
337
|
+
async initializeChannel() {
|
|
338
|
+
if (this.messageHandlerPromise) {
|
|
339
|
+
await this._resolveMessageHandlerPromise()
|
|
340
|
+
|
|
341
|
+
if (this.messageHandler) return
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (this.messageHandler) {
|
|
345
|
+
await this._runMessageHandlerOpen()
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const resolver = this.configuration.getWebsocketChannelResolver?.()
|
|
350
|
+
|
|
351
|
+
if (!resolver) return
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const tenant = await this._resolveTenant({})
|
|
355
|
+
const resolved = await this.configuration.runWithTenant(tenant, async () => {
|
|
356
|
+
return await resolver({
|
|
357
|
+
client: this.client,
|
|
358
|
+
configuration: this.configuration,
|
|
359
|
+
request: this.upgradeRequest,
|
|
360
|
+
websocketSession: this
|
|
361
|
+
})
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
if (!resolved) return
|
|
365
|
+
|
|
366
|
+
const channel = typeof resolved === "function"
|
|
367
|
+
? new resolved({client: this.client, configuration: this.configuration, request: this.upgradeRequest, websocketSession: this})
|
|
368
|
+
: resolved
|
|
369
|
+
|
|
370
|
+
if (channel && !(channel instanceof WebsocketChannel)) {
|
|
371
|
+
throw new Error("Resolved websocket channel must extend WebsocketChannel")
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
await this._registerChannel(channel, tenant)
|
|
375
|
+
} catch (error) {
|
|
376
|
+
this.logger.error(() => ["Failed to initialize websocket channel", error])
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Runs send goodbye.
|
|
382
|
+
* @param {import("./index.js").default} client - Client instance.
|
|
383
|
+
* @returns {void} - No return value.
|
|
384
|
+
*/
|
|
385
|
+
sendGoodbye(client) {
|
|
386
|
+
const frame = Buffer.from([WEBSOCKET_FINAL_FRAME | WEBSOCKET_OPCODE_CLOSE, 0x00])
|
|
387
|
+
|
|
388
|
+
client.events.emit("output", frame)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Runs handle message.
|
|
393
|
+
* @param {WebsocketSessionMessage} message - Message text.
|
|
394
|
+
* @returns {Promise<void>} - Resolves when complete.
|
|
395
|
+
*/
|
|
396
|
+
async _handleMessage(message) {
|
|
397
|
+
// Serialize per-session: chain onto `_messageChain` so messages
|
|
398
|
+
// are processed one at a time. Without this, fire-and-forget
|
|
399
|
+
// dispatch from `_processBuffer` lets message B read
|
|
400
|
+
// `session.data` before A has finished writing it.
|
|
401
|
+
const previous = this._messageChain
|
|
402
|
+
const next = previous.then(() => this._dispatchMessage(message))
|
|
403
|
+
|
|
404
|
+
this._messageChain = next.catch(() => {})
|
|
405
|
+
await next
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Runs dispatch message.
|
|
410
|
+
* @param {WebsocketSessionMessage} message - Message text.
|
|
411
|
+
* @returns {Promise<void>} - Resolves when complete.
|
|
412
|
+
*/
|
|
413
|
+
async _dispatchMessage(message) {
|
|
414
|
+
const wrapper = this.configuration.getWebsocketAroundRequest?.()
|
|
415
|
+
|
|
416
|
+
if (wrapper) {
|
|
417
|
+
await wrapper(this, () => this._handleMessageInner(message))
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
await this._handleMessageInner(message)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* The actual message dispatch, extracted so
|
|
426
|
+
* `configuration.getWebsocketAroundRequest()` can wrap it in any
|
|
427
|
+
* per-request context (AsyncLocalStorage, tracing, etc.).
|
|
428
|
+
* @param {WebsocketSessionMessage} message
|
|
429
|
+
* @returns {Promise<void>}
|
|
430
|
+
*/
|
|
431
|
+
async _handleMessageInner(message) {
|
|
432
|
+
if (this.pendingMessageHandler) {
|
|
433
|
+
this.messageQueue.push(message)
|
|
434
|
+
return
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// The messageHandler short-circuits default routing only when the
|
|
438
|
+
// app actually declared an `onMessage` hook. Apps that only want
|
|
439
|
+
// session-lifecycle tracking (`onOpen`/`onClose`) still need the
|
|
440
|
+
// built-in subscribe/connection/channel-subscribe routing below,
|
|
441
|
+
// otherwise every incoming message is silently dropped.
|
|
442
|
+
if (this.messageHandler && typeof this.messageHandler.onMessage === "function") {
|
|
443
|
+
await this._runMessageHandlerMessage(message)
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const subscribePayload = subscribeMessage(message)
|
|
448
|
+
|
|
449
|
+
if (subscribePayload) {
|
|
450
|
+
const {channel, lastEventId, params} = subscribePayload
|
|
451
|
+
|
|
452
|
+
if (!channel) throw new Error("channel is required for subscribe")
|
|
453
|
+
const resolver = this.configuration.getWebsocketChannelResolver?.()
|
|
454
|
+
|
|
455
|
+
if (resolver) {
|
|
456
|
+
await this._handleChannelSubscription({channel, lastEventId, params})
|
|
457
|
+
} else {
|
|
458
|
+
await this.subscribeToChannel(channel, {acknowledge: true, lastEventId, params})
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (message.type === "metadata") {
|
|
465
|
+
const metadataPayload = /**
|
|
466
|
+
* Narrows the runtime value to the documented type.
|
|
467
|
+
@type {{data?: Record<string, ?>}} */ (message)
|
|
468
|
+
|
|
469
|
+
this._metadata = metadataPayload.data && typeof metadataPayload.data === "object" ? {...metadataPayload.data} : {}
|
|
470
|
+
|
|
471
|
+
for (const {subscription} of this._channelSubscriptions.values()) {
|
|
472
|
+
if (typeof subscription.onMetadataChanged === "function") {
|
|
473
|
+
await this._withConnections(async () => {
|
|
474
|
+
await subscription.onMetadataChanged(this._metadata)
|
|
475
|
+
})
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (message.type === "session-resume") {
|
|
483
|
+
await this._handleSessionResume(message)
|
|
484
|
+
return
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (message.type === "connection-open") {
|
|
488
|
+
await this._handleConnectionOpen(message)
|
|
489
|
+
return
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (message.type === "connection-message") {
|
|
493
|
+
await this._handleConnectionMessage(message)
|
|
494
|
+
return
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (message.type === "connection-close") {
|
|
498
|
+
await this._handleConnectionClose(message)
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (message.type === "channel-subscribe") {
|
|
503
|
+
await this._handleChannelSubscribe(message)
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (message.type === "channel-unsubscribe") {
|
|
508
|
+
await this._handleChannelUnsubscribe(message)
|
|
509
|
+
return
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (message.type && message.type !== "request") {
|
|
513
|
+
this.sendJson({error: `Unknown message type: ${message.type}`, type: "error"})
|
|
514
|
+
return
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const requestPayload = requestMessage(message)
|
|
518
|
+
|
|
519
|
+
if (!requestPayload) {
|
|
520
|
+
this.sendJson({error: `Unknown message type: ${message.type}`, type: "error"})
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const {body, headers, id, method, path} = requestPayload
|
|
525
|
+
|
|
526
|
+
if (!method) throw new Error("method is required")
|
|
527
|
+
if (!path) throw new Error("path is required")
|
|
528
|
+
|
|
529
|
+
const request = new WebsocketRequest({
|
|
530
|
+
body,
|
|
531
|
+
headers,
|
|
532
|
+
metadata: this.getMetadata(),
|
|
533
|
+
method,
|
|
534
|
+
path,
|
|
535
|
+
remoteAddress: this.remoteAddress()
|
|
536
|
+
})
|
|
537
|
+
const requestRunner = new RequestRunner({
|
|
538
|
+
configuration: this.configuration,
|
|
539
|
+
request
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
requestRunner.events.on("done", () => {
|
|
543
|
+
const response = requestRunner.response
|
|
544
|
+
const body = response.getBody()
|
|
545
|
+
const headers = response.headers
|
|
546
|
+
|
|
547
|
+
this.sendJson({
|
|
548
|
+
body,
|
|
549
|
+
headers,
|
|
550
|
+
id,
|
|
551
|
+
statusCode: response.getStatusCode(),
|
|
552
|
+
statusMessage: response.getStatusMessage(),
|
|
553
|
+
type: "response"
|
|
554
|
+
})
|
|
555
|
+
void requestRunner.logCompletedRequest().catch((error) => {
|
|
556
|
+
this.logger.warn("Failed to log completed request", error)
|
|
557
|
+
})
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
await requestRunner.run()
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Runs process buffer.
|
|
565
|
+
* @returns {void} - No return value.
|
|
566
|
+
*/
|
|
567
|
+
_processBuffer() {
|
|
568
|
+
while (this.buffer.length >= 2) {
|
|
569
|
+
const firstByte = this.buffer[0]
|
|
570
|
+
const secondByte = this.buffer[1]
|
|
571
|
+
const isFinal = (firstByte & WEBSOCKET_FINAL_FRAME) === WEBSOCKET_FINAL_FRAME
|
|
572
|
+
const opcode = firstByte & 0x0F
|
|
573
|
+
const isMasked = (secondByte & 0x80) === 0x80
|
|
574
|
+
let payloadLength = secondByte & 0x7F
|
|
575
|
+
let offset = 2
|
|
576
|
+
|
|
577
|
+
if (payloadLength === 126) {
|
|
578
|
+
if (this.buffer.length < offset + 2) return
|
|
579
|
+
payloadLength = this.buffer.readUInt16BE(offset)
|
|
580
|
+
offset += 2
|
|
581
|
+
} else if (payloadLength === 127) {
|
|
582
|
+
if (this.buffer.length < offset + 8) return
|
|
583
|
+
const bigLength = this.buffer.readBigUInt64BE(offset)
|
|
584
|
+
|
|
585
|
+
payloadLength = Number(bigLength)
|
|
586
|
+
offset += 8
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const maskLength = isMasked ? 4 : 0
|
|
590
|
+
|
|
591
|
+
if (this.buffer.length < offset + maskLength + payloadLength) return
|
|
592
|
+
|
|
593
|
+
/** Payload. @type {Buffer} */
|
|
594
|
+
let payload = this.buffer.slice(offset + maskLength, offset + maskLength + payloadLength)
|
|
595
|
+
|
|
596
|
+
if (isMasked) {
|
|
597
|
+
const mask = this.buffer.slice(offset, offset + maskLength)
|
|
598
|
+
payload = this._unmaskPayload(payload, mask)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
this.buffer = this.buffer.slice(offset + maskLength + payloadLength)
|
|
602
|
+
|
|
603
|
+
// Control frames (opcode >= 0x8) must not be fragmented per
|
|
604
|
+
// RFC 6455 and can arrive interleaved with a fragmented data
|
|
605
|
+
// message. Handle them first without touching the fragment
|
|
606
|
+
// accumulator.
|
|
607
|
+
if (opcode === WEBSOCKET_OPCODE_PING) {
|
|
608
|
+
this._sendControlFrame(WEBSOCKET_OPCODE_PONG, payload)
|
|
609
|
+
continue
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (opcode === WEBSOCKET_OPCODE_CLOSE) {
|
|
613
|
+
this.sendGoodbye(this.client)
|
|
614
|
+
this._handleClose()
|
|
615
|
+
continue
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (opcode >= 0x8) {
|
|
619
|
+
this.logger.warn(`Unsupported websocket control opcode: ${opcode}`)
|
|
620
|
+
continue
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Data frame (TEXT/BINARY/CONTINUATION). Reassemble fragments
|
|
624
|
+
// before dispatching. Browsers (Chrome) legitimately fragment
|
|
625
|
+
// longer client→server text frames; a prior version dropped
|
|
626
|
+
// every fragmented message silently, so any payload large
|
|
627
|
+
// enough to hit the browser's fragmentation threshold
|
|
628
|
+
// (e.g. a channel-subscribe with an auth token) never reached
|
|
629
|
+
// the handler.
|
|
630
|
+
if (opcode === WEBSOCKET_OPCODE_CONTINUATION) {
|
|
631
|
+
if (this._fragmentedPayloads === null) {
|
|
632
|
+
this.logger.warn("Received continuation frame with no fragmented message in progress")
|
|
633
|
+
continue
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (!this._appendFragment(payload)) return
|
|
637
|
+
|
|
638
|
+
if (!isFinal) continue
|
|
639
|
+
} else if (opcode === WEBSOCKET_OPCODE_TEXT || opcode === WEBSOCKET_OPCODE_BINARY) {
|
|
640
|
+
if (this._fragmentedPayloads !== null) {
|
|
641
|
+
this.logger.warn("Received new data frame while a fragmented message was in progress; discarding prior fragments")
|
|
642
|
+
this._resetFragmentBuffer()
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (!isFinal) {
|
|
646
|
+
this._fragmentedPayloads = [payload]
|
|
647
|
+
this._fragmentedOpcode = opcode
|
|
648
|
+
this._fragmentedBytes = payload.length
|
|
649
|
+
|
|
650
|
+
if (!this._enforceFragmentLimits()) return
|
|
651
|
+
|
|
652
|
+
continue
|
|
653
|
+
}
|
|
654
|
+
} else {
|
|
655
|
+
this.logger.warn(`Unsupported websocket data opcode: ${opcode}`)
|
|
656
|
+
continue
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Defines finalPayload.
|
|
661
|
+
@type {Buffer} */
|
|
662
|
+
let finalPayload
|
|
663
|
+
/**
|
|
664
|
+
* Defines finalOpcode.
|
|
665
|
+
@type {number} */
|
|
666
|
+
let finalOpcode
|
|
667
|
+
|
|
668
|
+
if (this._fragmentedPayloads !== null) {
|
|
669
|
+
if (opcode === WEBSOCKET_OPCODE_CONTINUATION) {
|
|
670
|
+
finalPayload = Buffer.concat(this._fragmentedPayloads)
|
|
671
|
+
finalOpcode = this._fragmentedOpcode ?? WEBSOCKET_OPCODE_TEXT
|
|
672
|
+
} else {
|
|
673
|
+
finalPayload = payload
|
|
674
|
+
finalOpcode = opcode
|
|
675
|
+
}
|
|
676
|
+
this._resetFragmentBuffer()
|
|
677
|
+
} else {
|
|
678
|
+
finalPayload = payload
|
|
679
|
+
finalOpcode = opcode
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (finalOpcode !== WEBSOCKET_OPCODE_TEXT) {
|
|
683
|
+
this.logger.warn(`Unsupported websocket data opcode after reassembly: ${finalOpcode}`)
|
|
684
|
+
continue
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
try {
|
|
688
|
+
const message = JSON.parse(finalPayload.toString("utf-8"))
|
|
689
|
+
|
|
690
|
+
this._handleMessage(message).catch((error) => {
|
|
691
|
+
this.logger.error(() => ["Websocket message handler failed", error])
|
|
692
|
+
this.sendJson({error: error.message, type: "error"})
|
|
693
|
+
})
|
|
694
|
+
} catch (error) {
|
|
695
|
+
this.logger.error(() => ["Failed to parse websocket message", error])
|
|
696
|
+
this.sendJson({error: "Invalid websocket message", type: "error"})
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Appends a continuation-frame payload to the in-progress
|
|
703
|
+
* fragmented message. Returns true when the fragment was accepted
|
|
704
|
+
* and false when the per-message cap was hit and the socket has
|
|
705
|
+
* been closed.
|
|
706
|
+
* @param {Buffer} payload
|
|
707
|
+
* @returns {boolean}
|
|
708
|
+
*/
|
|
709
|
+
_appendFragment(payload) {
|
|
710
|
+
// Guard pushing first so `_enforceFragmentLimits` sees the final
|
|
711
|
+
// state; on overflow the reset inside the enforcer drops the
|
|
712
|
+
// buffered fragments.
|
|
713
|
+
this._fragmentedPayloads?.push(payload)
|
|
714
|
+
this._fragmentedBytes += payload.length
|
|
715
|
+
|
|
716
|
+
return this._enforceFragmentLimits()
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Verifies the fragmented message has not exceeded the byte or
|
|
721
|
+
* fragment-count caps. On overflow, clears the buffer, sends a
|
|
722
|
+
* close frame, and tears the session down. Returns true when the
|
|
723
|
+
* caller can continue processing, false when the session is being
|
|
724
|
+
* closed.
|
|
725
|
+
* @returns {boolean}
|
|
726
|
+
*/
|
|
727
|
+
_enforceFragmentLimits() {
|
|
728
|
+
if (this._fragmentedPayloads === null) return true
|
|
729
|
+
|
|
730
|
+
const fragmentCount = this._fragmentedPayloads.length
|
|
731
|
+
const overBytes = this._fragmentedBytes > WEBSOCKET_MAX_FRAGMENTED_MESSAGE_BYTES
|
|
732
|
+
const overFragments = fragmentCount > WEBSOCKET_MAX_FRAGMENTED_MESSAGE_FRAGMENTS
|
|
733
|
+
|
|
734
|
+
if (!overBytes && !overFragments) return true
|
|
735
|
+
|
|
736
|
+
this.logger.warn(() => [
|
|
737
|
+
"Fragmented websocket message exceeded caps; closing connection",
|
|
738
|
+
{
|
|
739
|
+
fragmentBytes: this._fragmentedBytes,
|
|
740
|
+
fragmentCount,
|
|
741
|
+
maxBytes: WEBSOCKET_MAX_FRAGMENTED_MESSAGE_BYTES,
|
|
742
|
+
maxFragments: WEBSOCKET_MAX_FRAGMENTED_MESSAGE_FRAGMENTS
|
|
743
|
+
}
|
|
744
|
+
])
|
|
745
|
+
|
|
746
|
+
this._resetFragmentBuffer()
|
|
747
|
+
this.buffer = Buffer.alloc(0)
|
|
748
|
+
this.sendGoodbye(this.client)
|
|
749
|
+
this._handleClose()
|
|
750
|
+
|
|
751
|
+
return false
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Runs reset fragment buffer.
|
|
756
|
+
@returns {void} */
|
|
757
|
+
_resetFragmentBuffer() {
|
|
758
|
+
this._fragmentedPayloads = null
|
|
759
|
+
this._fragmentedOpcode = null
|
|
760
|
+
this._fragmentedBytes = 0
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Runs send control frame.
|
|
765
|
+
* @param {number} opcode - Opcode.
|
|
766
|
+
* @param {Buffer} payload - Payload data.
|
|
767
|
+
* @returns {void} - No return value.
|
|
768
|
+
*/
|
|
769
|
+
_sendControlFrame(opcode, payload) {
|
|
770
|
+
const header = Buffer.alloc(2)
|
|
771
|
+
|
|
772
|
+
header[0] = WEBSOCKET_FINAL_FRAME | opcode
|
|
773
|
+
header[1] = payload.length
|
|
774
|
+
|
|
775
|
+
this.client.events.emit("output", Buffer.concat([header, payload]))
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Runs send json.
|
|
780
|
+
* @param {object} body - Request body.
|
|
781
|
+
* @returns {void} - No return value.
|
|
782
|
+
*/
|
|
783
|
+
sendJson(body) {
|
|
784
|
+
// While paused (waiting for a resume), stash frames in an
|
|
785
|
+
// outbound queue and flush them in order on resume. Capped to
|
|
786
|
+
// prevent runaway memory use while the client is offline.
|
|
787
|
+
if (this._paused) {
|
|
788
|
+
this._outboundQueue ||= []
|
|
789
|
+
|
|
790
|
+
if (this._outboundQueue.length >= WEBSOCKET_PAUSED_QUEUE_CAP) {
|
|
791
|
+
// Drop oldest so the most recent activity wins on resume.
|
|
792
|
+
this._outboundQueue.shift()
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
this._outboundQueue.push(body)
|
|
796
|
+
return
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (!this.client?.events) return
|
|
800
|
+
|
|
801
|
+
const json = JSON.stringify(body)
|
|
802
|
+
const payload = Buffer.from(json, "utf-8")
|
|
803
|
+
let header
|
|
804
|
+
|
|
805
|
+
if (payload.length < 126) {
|
|
806
|
+
header = Buffer.alloc(2)
|
|
807
|
+
header[1] = payload.length
|
|
808
|
+
} else if (payload.length < 65536) {
|
|
809
|
+
header = Buffer.alloc(4)
|
|
810
|
+
header[1] = 126
|
|
811
|
+
header.writeUInt16BE(payload.length, 2)
|
|
812
|
+
} else {
|
|
813
|
+
header = Buffer.alloc(10)
|
|
814
|
+
header[1] = 127
|
|
815
|
+
header.writeBigUInt64BE(BigInt(payload.length), 2)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
header[0] = WEBSOCKET_FINAL_FRAME | WEBSOCKET_OPCODE_TEXT
|
|
819
|
+
|
|
820
|
+
this.client.events.emit("output", Buffer.concat([header, payload]))
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Flushes the paused outbound queue over the current socket.
|
|
825
|
+
* Called during resume after `session-resumed` has been sent on
|
|
826
|
+
* the NEW session's socket (not this session's).
|
|
827
|
+
* @returns {void}
|
|
828
|
+
*/
|
|
829
|
+
_flushOutboundQueue() {
|
|
830
|
+
const queue = this._outboundQueue || []
|
|
831
|
+
|
|
832
|
+
this._outboundQueue = []
|
|
833
|
+
|
|
834
|
+
for (const body of queue) {
|
|
835
|
+
this.sendJson(body)
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Runs subscribe to channel.
|
|
841
|
+
* @param {string} channel - Channel name.
|
|
842
|
+
* @param {{acknowledge?: boolean, channelHandler?: import("../websocket-channel.js").default, lastEventId?: string, params?: Record<string, ?>, subscriptionChannel?: string}} [options] - Subscribe options.
|
|
843
|
+
* @returns {Promise<boolean>} - Whether the subscription was added.
|
|
844
|
+
*/
|
|
845
|
+
async subscribeToChannel(channel, {acknowledge = true, channelHandler, lastEventId, params, subscriptionChannel} = {}) {
|
|
846
|
+
await websocketEventLogStoreForConfiguration(this.configuration).markChannelInterested(channel)
|
|
847
|
+
|
|
848
|
+
const replayState = await this._prepareReplayState({
|
|
849
|
+
channel,
|
|
850
|
+
lastEventId,
|
|
851
|
+
subscriptionChannel: subscriptionChannel || channel,
|
|
852
|
+
subscriptionParams: params
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
if (replayState === false) return false
|
|
856
|
+
if (replayState) {
|
|
857
|
+
this.channelReplayStates.set(channel, replayState)
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
this.addSubscription(channel)
|
|
861
|
+
|
|
862
|
+
if (channelHandler) {
|
|
863
|
+
if (!this.subscriptionHandlers.has(channel)) {
|
|
864
|
+
this.subscriptionHandlers.set(channel, new Set())
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
this.subscriptionHandlers.get(channel)?.add(channelHandler)
|
|
868
|
+
|
|
869
|
+
if (!this.handlerSubscriptions.has(channelHandler)) {
|
|
870
|
+
this.handlerSubscriptions.set(channelHandler, new Set())
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
this.handlerSubscriptions.get(channelHandler)?.add(channel)
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (replayState) {
|
|
877
|
+
try {
|
|
878
|
+
await this._replayChannelEvents({channel, replayState})
|
|
879
|
+
} finally {
|
|
880
|
+
await this._finishReplayState(channel, replayState)
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (acknowledge) {
|
|
885
|
+
this.sendJson({channel, type: "subscribed"})
|
|
886
|
+
}
|
|
887
|
+
return true
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
_handleClose() {
|
|
891
|
+
// If the session has resumable state (live Connection or
|
|
892
|
+
// ChannelV2 subscription), move it into the paused registry
|
|
893
|
+
// instead of tearing down; a new socket presenting the sessionId
|
|
894
|
+
// via `session-resume` within the grace window will reattach.
|
|
895
|
+
const hasResumableState = this._connections.size > 0 || this._channelSubscriptions.size > 0
|
|
896
|
+
|
|
897
|
+
if (hasResumableState && !this._paused) {
|
|
898
|
+
this._paused = true
|
|
899
|
+
this.socket = null
|
|
900
|
+
// Kick off auth-identity capture for resume verification. Runs
|
|
901
|
+
// in the background — `_handleSessionResume` awaits
|
|
902
|
+
// `_resumeIdentityPromise` before comparing. Pause registration
|
|
903
|
+
// is synchronous so a resume arriving immediately still finds
|
|
904
|
+
// the session.
|
|
905
|
+
this._resumeIdentityPromise = this._captureResumeIdentity()
|
|
906
|
+
void this._fireOnDisconnect()
|
|
907
|
+
this.configuration._pauseWebsocketSession(this)
|
|
908
|
+
this.events.emit("close")
|
|
909
|
+
return
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
void this._runMessageHandlerClose()
|
|
913
|
+
void this._teardownChannel()
|
|
914
|
+
void this._teardownConnections("session_destroyed")
|
|
915
|
+
void this._teardownChannelSubscriptions()
|
|
916
|
+
this.events.emit("close")
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Called by the grace timer when the paused period expires without
|
|
921
|
+
* a resume. Tears down all live Connections + Channel subs and
|
|
922
|
+
* drops the session.
|
|
923
|
+
* @returns {void}
|
|
924
|
+
*/
|
|
925
|
+
_finalizeGraceExpiry() {
|
|
926
|
+
void this._runMessageHandlerClose()
|
|
927
|
+
void this._teardownChannel()
|
|
928
|
+
void this._teardownConnections("grace_expired")
|
|
929
|
+
void this._teardownChannelSubscriptions()
|
|
930
|
+
this.events.emit("close")
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Runs the configured identity resolver against this session.
|
|
935
|
+
* The returned promise is stored at pause time and awaited at
|
|
936
|
+
* resume time so we can reject resume attempts from a different
|
|
937
|
+
* authenticated caller (signed out, swapped user, expired cookie).
|
|
938
|
+
* @returns {Promise<?>}
|
|
939
|
+
*/
|
|
940
|
+
async _captureResumeIdentity() {
|
|
941
|
+
const resolver = this.configuration.getWebsocketSessionIdentityResolver?.()
|
|
942
|
+
|
|
943
|
+
if (typeof resolver !== "function") return undefined
|
|
944
|
+
|
|
945
|
+
try {
|
|
946
|
+
return await resolver(this)
|
|
947
|
+
} catch (error) {
|
|
948
|
+
this.logger.error(() => ["Websocket session identity resolver failed at pause", error])
|
|
949
|
+
return undefined
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Fires `onDisconnect` on every live Connection and Channel sub so
|
|
955
|
+
* apps can pause per-instance work while the session is paused.
|
|
956
|
+
* Errors are logged, not rethrown — one broken handler must not
|
|
957
|
+
* block the rest.
|
|
958
|
+
* @returns {Promise<void>}
|
|
959
|
+
*/
|
|
960
|
+
async _fireOnDisconnect() {
|
|
961
|
+
await this._fireLifecycleCallback("onDisconnect")
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Fires `onResume` on every live Connection and Channel sub after
|
|
966
|
+
* a successful `session-resume` handoff.
|
|
967
|
+
* @returns {Promise<void>}
|
|
968
|
+
*/
|
|
969
|
+
async _fireOnResume() {
|
|
970
|
+
await this._fireLifecycleCallback("onResume")
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Runs fire lifecycle callback.
|
|
975
|
+
* @param {"onDisconnect" | "onResume"} callbackName Lifecycle callback to fire.
|
|
976
|
+
* @returns {Promise<void>} Resolves when every live handler has been attempted.
|
|
977
|
+
*/
|
|
978
|
+
async _fireLifecycleCallback(callbackName) {
|
|
979
|
+
for (const connection of this._connections.values()) {
|
|
980
|
+
try {
|
|
981
|
+
await connection[callbackName]?.()
|
|
982
|
+
} catch (error) {
|
|
983
|
+
this.logger.error(() => [`${callbackName} failed for ${connection.connectionId}`, error])
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
for (const {subscription} of this._channelSubscriptions.values()) {
|
|
988
|
+
try {
|
|
989
|
+
await subscription[callbackName]?.()
|
|
990
|
+
} catch (error) {
|
|
991
|
+
this.logger.error(() => [`${callbackName} failed for channel sub ${subscription.subscriptionId}`, error])
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Handles `{type: "session-resume"}`. This session (the newly-
|
|
998
|
+
* created one whose socket just connected) transfers state from
|
|
999
|
+
* the paused session and instructs the client via
|
|
1000
|
+
* `session-resumed` or `session-gone`.
|
|
1001
|
+
* @param {Record<string, ?>} message
|
|
1002
|
+
* @returns {Promise<void>}
|
|
1003
|
+
*/
|
|
1004
|
+
async _handleSessionResume(message) {
|
|
1005
|
+
const resumeSessionId = message.sessionId
|
|
1006
|
+
|
|
1007
|
+
if (typeof resumeSessionId !== "string" || !resumeSessionId) {
|
|
1008
|
+
this.sendJson({type: "session-gone"})
|
|
1009
|
+
return
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const paused = this.configuration._findPausedWebsocketSession(resumeSessionId)
|
|
1013
|
+
|
|
1014
|
+
if (!paused) {
|
|
1015
|
+
this.sendJson({type: "session-gone"})
|
|
1016
|
+
return
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Auth re-verify: compare the fresh caller's identity against the
|
|
1020
|
+
// one captured at pause. Mismatch means a different user (or a
|
|
1021
|
+
// signed-out session) is trying to reclaim state that isn't
|
|
1022
|
+
// theirs — destroy the paused session outright.
|
|
1023
|
+
const resolver = this.configuration.getWebsocketSessionIdentityResolver?.()
|
|
1024
|
+
|
|
1025
|
+
if (typeof resolver === "function") {
|
|
1026
|
+
const pausedIdentity = await paused._resumeIdentityPromise
|
|
1027
|
+
let freshIdentity
|
|
1028
|
+
|
|
1029
|
+
try {
|
|
1030
|
+
freshIdentity = await resolver(this)
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
this.logger.error(() => ["Websocket session identity resolver failed at resume", error])
|
|
1033
|
+
freshIdentity = undefined
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (!identitiesMatch(pausedIdentity, freshIdentity)) {
|
|
1037
|
+
this.configuration._clearPausedWebsocketSession(resumeSessionId)
|
|
1038
|
+
paused._finalizeGraceExpiry()
|
|
1039
|
+
this.sendJson({type: "session-gone"})
|
|
1040
|
+
return
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
this.configuration._clearPausedWebsocketSession(resumeSessionId)
|
|
1045
|
+
|
|
1046
|
+
// Transfer resumable state onto this (live) session. The paused
|
|
1047
|
+
// session shell is discarded after the transfer.
|
|
1048
|
+
for (const [connectionId, connection] of paused._connections) {
|
|
1049
|
+
connection.session = this
|
|
1050
|
+
this._connections.set(connectionId, connection)
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
for (const [subId, entry] of paused._channelSubscriptions) {
|
|
1054
|
+
entry.subscription.session = this
|
|
1055
|
+
this._channelSubscriptions.set(subId, entry)
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
this._metadata = {...paused._metadata}
|
|
1059
|
+
this.data = paused.data
|
|
1060
|
+
this.sessionId = resumeSessionId
|
|
1061
|
+
|
|
1062
|
+
// Transfer any frames queued while the paused session had no
|
|
1063
|
+
// socket. They flush AFTER session-resumed so the client knows
|
|
1064
|
+
// which session they belong to.
|
|
1065
|
+
const queued = paused._outboundQueue || []
|
|
1066
|
+
|
|
1067
|
+
paused._outboundQueue = []
|
|
1068
|
+
paused._connections.clear()
|
|
1069
|
+
paused._channelSubscriptions.clear()
|
|
1070
|
+
paused._paused = false
|
|
1071
|
+
paused.destroy()
|
|
1072
|
+
|
|
1073
|
+
this.sendJson({type: "session-resumed", sessionId: resumeSessionId})
|
|
1074
|
+
for (const body of queued) this.sendJson(body)
|
|
1075
|
+
await this._fireOnResume()
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Fires `onClose(reason)` on every live app-defined connection, then
|
|
1080
|
+
* drops them from the registry. No network frame is sent — the
|
|
1081
|
+
* socket is already going away.
|
|
1082
|
+
* @param {"session_destroyed" | "grace_expired" | "error"} reason
|
|
1083
|
+
* @returns {Promise<void>}
|
|
1084
|
+
*/
|
|
1085
|
+
async _teardownConnections(reason) {
|
|
1086
|
+
const connections = [...this._connections.values()]
|
|
1087
|
+
|
|
1088
|
+
this._connections.clear()
|
|
1089
|
+
|
|
1090
|
+
for (const connection of connections) {
|
|
1091
|
+
connection._closed = true
|
|
1092
|
+
|
|
1093
|
+
try {
|
|
1094
|
+
await this._withConnections(async () => {
|
|
1095
|
+
await connection.onClose(reason)
|
|
1096
|
+
})
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
this.logger.error(() => [`Failed to tear down connection ${connection.connectionId}`, error])
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* Handles a `{type: "connection-open"}` message — instantiates the
|
|
1105
|
+
* registered connection class, stores it on `_connections`, and
|
|
1106
|
+
* fires `onConnect()`. Sends `connection-opened` on success or
|
|
1107
|
+
* `connection-error` on failure.
|
|
1108
|
+
* @param {Record<string, ?>} message
|
|
1109
|
+
* @returns {Promise<void>}
|
|
1110
|
+
*/
|
|
1111
|
+
async _handleConnectionOpen(message) {
|
|
1112
|
+
const connectionId = message.connectionId
|
|
1113
|
+
const connectionType = message.connectionType
|
|
1114
|
+
const params = message.params || {}
|
|
1115
|
+
|
|
1116
|
+
if (typeof connectionId !== "string" || !connectionId) {
|
|
1117
|
+
this.sendJson({type: "error", error: "connection-open requires connectionId"})
|
|
1118
|
+
return
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
if (typeof connectionType !== "string" || !connectionType) {
|
|
1122
|
+
this.sendJson({type: "connection-error", connectionId, message: "connectionType is required"})
|
|
1123
|
+
return
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if (this._connections.has(connectionId)) {
|
|
1127
|
+
this.sendJson({type: "connection-error", connectionId, message: "Connection id already in use"})
|
|
1128
|
+
return
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
const ConnectionClass = this.configuration.getWebsocketConnectionClass?.(connectionType)
|
|
1132
|
+
|
|
1133
|
+
if (!ConnectionClass) {
|
|
1134
|
+
this.sendJson({type: "connection-error", connectionId, message: `Unknown connection type: ${connectionType}`})
|
|
1135
|
+
return
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const connection = new ConnectionClass({connectionId, params, session: this})
|
|
1139
|
+
|
|
1140
|
+
try {
|
|
1141
|
+
await this._withConnections(async () => {
|
|
1142
|
+
await connection.onConnect()
|
|
1143
|
+
})
|
|
1144
|
+
// Register only after onConnect resolves so a connection-message
|
|
1145
|
+
// can never be routed to a partially initialized connection.
|
|
1146
|
+
this._connections.set(connectionId, connection)
|
|
1147
|
+
this.sendJson({type: "connection-opened", connectionId})
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
this.logger.error(() => [`Failed to open connection ${connectionType}:${connectionId}`, error])
|
|
1150
|
+
this.sendJson({type: "connection-error", connectionId, message: /**
|
|
1151
|
+
* Narrows the runtime value to the documented type.
|
|
1152
|
+
@type {Error} */ (error).message || "Failed to open connection"})
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Handles a `{type: "connection-message"}` from the client.
|
|
1158
|
+
* @param {Record<string, ?>} message
|
|
1159
|
+
* @returns {Promise<void>}
|
|
1160
|
+
*/
|
|
1161
|
+
async _handleConnectionMessage(message) {
|
|
1162
|
+
const connectionId = message.connectionId
|
|
1163
|
+
const connection = typeof connectionId === "string" ? this._connections.get(connectionId) : null
|
|
1164
|
+
|
|
1165
|
+
if (!connection) {
|
|
1166
|
+
this.sendJson({type: "connection-error", connectionId, message: "Unknown connection id"})
|
|
1167
|
+
return
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
try {
|
|
1171
|
+
await this._withConnections(async () => {
|
|
1172
|
+
await connection.onMessage(message.body)
|
|
1173
|
+
})
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
this.logger.error(() => [`Failed to handle connection-message for ${connectionId}`, error])
|
|
1176
|
+
this.sendJson({type: "connection-error", connectionId, message: /**
|
|
1177
|
+
* Narrows the runtime value to the documented type.
|
|
1178
|
+
@type {Error} */ (error).message || "Failed to handle message"})
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* Handles a `{type: "connection-close"}` from the client — fires
|
|
1184
|
+
* `onClose("client_close")` and confirms with `connection-closed`.
|
|
1185
|
+
* @param {Record<string, ?>} message
|
|
1186
|
+
* @returns {Promise<void>}
|
|
1187
|
+
*/
|
|
1188
|
+
async _handleConnectionClose(message) {
|
|
1189
|
+
const connectionId = message.connectionId
|
|
1190
|
+
const connection = typeof connectionId === "string" ? this._connections.get(connectionId) : null
|
|
1191
|
+
|
|
1192
|
+
if (!connection) return
|
|
1193
|
+
|
|
1194
|
+
this._connections.delete(connectionId)
|
|
1195
|
+
// Mark closed before firing onClose so app code holding the
|
|
1196
|
+
// handle sees `isClosed() === true` and can't re-enter sendMessage.
|
|
1197
|
+
connection._closed = true
|
|
1198
|
+
|
|
1199
|
+
try {
|
|
1200
|
+
await this._withConnections(async () => {
|
|
1201
|
+
await connection.onClose("client_close")
|
|
1202
|
+
})
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
this.logger.error(() => [`Failed to tear down connection ${connectionId}`, error])
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
this.sendJson({type: "connection-closed", connectionId, reason: "client_close"})
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Handles `{type: "channel-subscribe"}` — runs `canSubscribe()`,
|
|
1212
|
+
* registers with the Configuration's global routing registry on
|
|
1213
|
+
* success, and sends `channel-subscribed` or `channel-error`.
|
|
1214
|
+
* @param {Record<string, ?>} message
|
|
1215
|
+
* @returns {Promise<void>}
|
|
1216
|
+
*/
|
|
1217
|
+
async _handleChannelSubscribe(message) {
|
|
1218
|
+
const subscriptionId = message.subscriptionId
|
|
1219
|
+
const channelType = message.channelType
|
|
1220
|
+
const params = message.params || {}
|
|
1221
|
+
const lastEventId = message.lastEventId
|
|
1222
|
+
|
|
1223
|
+
if (typeof subscriptionId !== "string" || !subscriptionId) {
|
|
1224
|
+
this.sendJson({type: "error", error: "channel-subscribe requires subscriptionId"})
|
|
1225
|
+
return
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (typeof channelType !== "string" || !channelType) {
|
|
1229
|
+
this.sendJson({type: "channel-error", subscriptionId, message: "channelType is required"})
|
|
1230
|
+
return
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (this._channelSubscriptions.has(subscriptionId)) {
|
|
1234
|
+
this.sendJson({type: "channel-error", subscriptionId, message: "Subscription id already in use"})
|
|
1235
|
+
return
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const ChannelClass = this.configuration.getWebsocketChannelClass?.(channelType)
|
|
1239
|
+
|
|
1240
|
+
if (!ChannelClass) {
|
|
1241
|
+
this.sendJson({type: "channel-error", subscriptionId, message: `Unknown channel type: ${channelType}`})
|
|
1242
|
+
return
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
const subscription = new ChannelClass({subscriptionId, params, session: this})
|
|
1246
|
+
|
|
1247
|
+
try {
|
|
1248
|
+
// Resolving the tenant can run database queries (e.g. looking up the
|
|
1249
|
+
// record's project and the caller's access), so it must happen inside a
|
|
1250
|
+
// connection scope. Without this the resolver borrows a connection that
|
|
1251
|
+
// is checked back in before/while it queries, intermittently surfacing as
|
|
1252
|
+
// "Connection … doesn't exist any more" or a falsely unauthorized
|
|
1253
|
+
// subscription.
|
|
1254
|
+
let tenant
|
|
1255
|
+
await this._withConnections(async () => {
|
|
1256
|
+
tenant = await this._resolveTenant({channel: channelType, params})
|
|
1257
|
+
})
|
|
1258
|
+
|
|
1259
|
+
await this.configuration.runWithTenant(tenant, async () => {
|
|
1260
|
+
let allowed = false
|
|
1261
|
+
|
|
1262
|
+
await this._withConnections(async () => {
|
|
1263
|
+
allowed = Boolean(await subscription.canSubscribe())
|
|
1264
|
+
})
|
|
1265
|
+
|
|
1266
|
+
if (!allowed) {
|
|
1267
|
+
this.sendJson({type: "channel-error", subscriptionId, message: "Subscription not authorized"})
|
|
1268
|
+
return
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
this._channelSubscriptions.set(subscriptionId, {channelType, subscription})
|
|
1272
|
+
this.configuration._registerWebsocketChannelSubscription(channelType, subscription)
|
|
1273
|
+
|
|
1274
|
+
await this._withConnections(async () => await subscription.subscribed())
|
|
1275
|
+
|
|
1276
|
+
// Replay missed events BEFORE sending channel-subscribed so
|
|
1277
|
+
// the client knows: everything before the confirmation is
|
|
1278
|
+
// replayed, everything after is live.
|
|
1279
|
+
if (typeof lastEventId === "string" && lastEventId.length > 0) {
|
|
1280
|
+
await this._replayChannelEventsForSubscription({channelType, lastEventId, subscription})
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
this.sendJson({type: "channel-subscribed", subscriptionId})
|
|
1284
|
+
})
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
this._channelSubscriptions.delete(subscriptionId)
|
|
1287
|
+
this.configuration._unregisterWebsocketChannelSubscription(channelType, subscription)
|
|
1288
|
+
this.logger.error(() => [`Failed to subscribe channel ${channelType}:${subscriptionId}`, error])
|
|
1289
|
+
this.sendJson({type: "channel-error", subscriptionId, message: /**
|
|
1290
|
+
* Narrows the runtime value to the documented type.
|
|
1291
|
+
@type {Error} */ (error).message || "Failed to subscribe"})
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* Replays missed events from the persistent event-log store for a
|
|
1297
|
+
* channel subscription that provided `lastEventId`. Sends each
|
|
1298
|
+
* missed event as a `channel-message` with `replayed: true`.
|
|
1299
|
+
* @param {object} args - Options.
|
|
1300
|
+
* @param {string} args.channelType - Channel type name (event-log key).
|
|
1301
|
+
* @param {string} args.lastEventId - Client's last-seen event id.
|
|
1302
|
+
* @param {import("../websocket-channel.js").default} args.subscription - Live subscription.
|
|
1303
|
+
* @returns {Promise<void>}
|
|
1304
|
+
*/
|
|
1305
|
+
async _replayChannelEventsForSubscription({channelType, lastEventId, subscription}) {
|
|
1306
|
+
const store = websocketEventLogStoreForConfiguration(this.configuration)
|
|
1307
|
+
|
|
1308
|
+
await this.configuration.awaitPendingBroadcasts()
|
|
1309
|
+
|
|
1310
|
+
const checkpoint = await store.getEventById({channel: channelType, id: lastEventId})
|
|
1311
|
+
|
|
1312
|
+
if (!checkpoint) {
|
|
1313
|
+
this.sendJson({
|
|
1314
|
+
type: "channel-replay-gap",
|
|
1315
|
+
subscriptionId: subscription.subscriptionId,
|
|
1316
|
+
lastEventId
|
|
1317
|
+
})
|
|
1318
|
+
return
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const ceiling = await store.latestSequence(channelType)
|
|
1322
|
+
|
|
1323
|
+
if (!ceiling || ceiling <= checkpoint.sequence) return
|
|
1324
|
+
|
|
1325
|
+
const events = await store.getEventsAfter({
|
|
1326
|
+
channel: channelType,
|
|
1327
|
+
sequence: checkpoint.sequence,
|
|
1328
|
+
upToSequence: ceiling
|
|
1329
|
+
})
|
|
1330
|
+
|
|
1331
|
+
for (const event of events) {
|
|
1332
|
+
if (subscription.isClosed()) break
|
|
1333
|
+
|
|
1334
|
+
subscription.sendMessage(/**
|
|
1335
|
+
* Narrows the runtime value to the documented type.
|
|
1336
|
+
@type {import("../websocket-channel.js").WebsocketJsonValue} */ (event.payload))
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* Handles `{type: "channel-unsubscribe"}` from the client — calls
|
|
1342
|
+
* `unsubscribed()` and sends `channel-unsubscribed`.
|
|
1343
|
+
* @param {Record<string, ?>} message
|
|
1344
|
+
* @returns {Promise<void>}
|
|
1345
|
+
*/
|
|
1346
|
+
async _handleChannelUnsubscribe(message) {
|
|
1347
|
+
const subscriptionId = message.subscriptionId
|
|
1348
|
+
|
|
1349
|
+
if (typeof subscriptionId !== "string") return
|
|
1350
|
+
|
|
1351
|
+
const entry = this._channelSubscriptions.get(subscriptionId)
|
|
1352
|
+
|
|
1353
|
+
if (!entry) return
|
|
1354
|
+
|
|
1355
|
+
this._channelSubscriptions.delete(subscriptionId)
|
|
1356
|
+
this.configuration._unregisterWebsocketChannelSubscription(entry.channelType, entry.subscription)
|
|
1357
|
+
entry.subscription._closed = true
|
|
1358
|
+
|
|
1359
|
+
try {
|
|
1360
|
+
await this._withConnections(async () => await entry.subscription.unsubscribed())
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
this.logger.error(() => [`Failed to unsubscribe channel ${entry.channelType}:${subscriptionId}`, error])
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
this.sendJson({type: "channel-unsubscribed", subscriptionId})
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* Fires `unsubscribed()` on every live channel-v2 subscription,
|
|
1370
|
+
* removes them from the Configuration's global registry, and
|
|
1371
|
+
* drops the session's own map. No network frames — the socket
|
|
1372
|
+
* is already going away.
|
|
1373
|
+
* @returns {Promise<void>}
|
|
1374
|
+
*/
|
|
1375
|
+
async _teardownChannelSubscriptions() {
|
|
1376
|
+
const entries = [...this._channelSubscriptions.values()]
|
|
1377
|
+
|
|
1378
|
+
this._channelSubscriptions.clear()
|
|
1379
|
+
|
|
1380
|
+
for (const {channelType, subscription} of entries) {
|
|
1381
|
+
this.configuration._unregisterWebsocketChannelSubscription(channelType, subscription)
|
|
1382
|
+
subscription._closed = true
|
|
1383
|
+
|
|
1384
|
+
try {
|
|
1385
|
+
await this._withConnections(async () => await subscription.unsubscribed())
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
this.logger.error(() => [`Failed to tear down channel-v2 ${channelType}:${subscription.subscriptionId}`, error])
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
async _teardownChannel() {
|
|
1393
|
+
for (const channel of this.channels) {
|
|
1394
|
+
await this._teardownSingleChannel(channel)
|
|
1395
|
+
}
|
|
1396
|
+
this.channels.clear()
|
|
1397
|
+
this.channelReplayStates.clear()
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* Runs teardown single channel.
|
|
1402
|
+
* @param {WebsocketChannel} channel - Channel instance.
|
|
1403
|
+
* @returns {Promise<void>} - Resolves when complete.
|
|
1404
|
+
*/
|
|
1405
|
+
async _teardownSingleChannel(channel) {
|
|
1406
|
+
try {
|
|
1407
|
+
const tenant = this.channelTenants.get(channel)
|
|
1408
|
+
|
|
1409
|
+
await this.configuration.runWithTenant(tenant, async () => {
|
|
1410
|
+
await this._withConnections(async () => {
|
|
1411
|
+
await channel?.unsubscribed?.()
|
|
1412
|
+
})
|
|
1413
|
+
})
|
|
1414
|
+
} catch (error) {
|
|
1415
|
+
this.logger.error(() => ["Failed to teardown websocket channel", error])
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
const subscriptions = this.handlerSubscriptions.get(channel)
|
|
1419
|
+
|
|
1420
|
+
if (subscriptions) {
|
|
1421
|
+
for (const subscriptionChannel of subscriptions) {
|
|
1422
|
+
this.subscriptionHandlers.get(subscriptionChannel)?.delete(channel)
|
|
1423
|
+
|
|
1424
|
+
if (this.subscriptionHandlers.get(subscriptionChannel)?.size === 0) {
|
|
1425
|
+
this.subscriptionHandlers.delete(subscriptionChannel)
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
this.handlerSubscriptions.delete(channel)
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
this.channelTenants.delete(channel)
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Runs register channel.
|
|
1437
|
+
* @param {WebsocketChannel | undefined} channel - Channel instance.
|
|
1438
|
+
* @param {string | null | undefined} tenant - Tenant key.
|
|
1439
|
+
* @returns {Promise<void>} - Resolves when complete.
|
|
1440
|
+
*/
|
|
1441
|
+
async _registerChannel(channel, tenant) {
|
|
1442
|
+
if (!channel) return
|
|
1443
|
+
|
|
1444
|
+
this.channels.add(channel)
|
|
1445
|
+
this.channelTenants.set(channel, tenant)
|
|
1446
|
+
await this.configuration.runWithTenant(tenant, async () => {
|
|
1447
|
+
await this._withConnections(async () => {
|
|
1448
|
+
await channel?.subscribed?.()
|
|
1449
|
+
})
|
|
1450
|
+
})
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/**
|
|
1454
|
+
* Runs with connections.
|
|
1455
|
+
* @param {() => Promise<void>} callback - Callback.
|
|
1456
|
+
* @returns {Promise<void>} - Resolves when complete.
|
|
1457
|
+
*/
|
|
1458
|
+
async _withConnections(callback) {
|
|
1459
|
+
await this.configuration.ensureConnections({name: "Websocket session"}, async () => {
|
|
1460
|
+
await callback()
|
|
1461
|
+
})
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* Runs handle channel subscription.
|
|
1466
|
+
* @param {{channel: string, lastEventId?: string, params?: Record<string, ?>}} args - Subscription args.
|
|
1467
|
+
* @returns {Promise<void>} - Resolves when complete.
|
|
1468
|
+
*/
|
|
1469
|
+
async _handleChannelSubscription({channel, lastEventId, params}) {
|
|
1470
|
+
const resolver = this.configuration.getWebsocketChannelResolver?.()
|
|
1471
|
+
|
|
1472
|
+
if (!resolver) return
|
|
1473
|
+
|
|
1474
|
+
try {
|
|
1475
|
+
// Tenant resolution can run database queries, so it must happen inside a
|
|
1476
|
+
// connection scope (see _handleChannelSubscribe).
|
|
1477
|
+
let tenant
|
|
1478
|
+
await this._withConnections(async () => {
|
|
1479
|
+
tenant = await this._resolveTenant({channel, params})
|
|
1480
|
+
})
|
|
1481
|
+
const resolved = await this.configuration.runWithTenant(tenant, async () => {
|
|
1482
|
+
return await resolver({
|
|
1483
|
+
client: this.client,
|
|
1484
|
+
configuration: this.configuration,
|
|
1485
|
+
request: this.upgradeRequest,
|
|
1486
|
+
subscription: {channel, params},
|
|
1487
|
+
websocketSession: this
|
|
1488
|
+
})
|
|
1489
|
+
})
|
|
1490
|
+
|
|
1491
|
+
if (!resolved) {
|
|
1492
|
+
this.sendJson({channel, error: "Subscription rejected", type: "error"})
|
|
1493
|
+
return
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
const channelInstance = typeof resolved === "function"
|
|
1497
|
+
? new resolved({
|
|
1498
|
+
client: this.client,
|
|
1499
|
+
configuration: this.configuration,
|
|
1500
|
+
lastEventId,
|
|
1501
|
+
request: this.upgradeRequest,
|
|
1502
|
+
subscriptionChannel: channel,
|
|
1503
|
+
subscriptionParams: params,
|
|
1504
|
+
websocketSession: this
|
|
1505
|
+
})
|
|
1506
|
+
: resolved
|
|
1507
|
+
|
|
1508
|
+
if (channelInstance && !(channelInstance instanceof WebsocketChannel)) {
|
|
1509
|
+
throw new Error("Resolved websocket channel must extend WebsocketChannel")
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
await this._registerChannel(channelInstance, tenant)
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
this.logger.warn(() => ["Websocket channel subscription failed", error])
|
|
1515
|
+
this.sendJson({channel, error: "Subscription rejected", type: "error"})
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* Runs prepare replay state.
|
|
1521
|
+
* @param {object} args - Options.
|
|
1522
|
+
* @param {string} args.channel - Internal channel name.
|
|
1523
|
+
* @param {string | undefined} args.lastEventId - Last received event id.
|
|
1524
|
+
* @param {string} args.subscriptionChannel - Client-facing channel name.
|
|
1525
|
+
* @param {Record<string, ?> | undefined} args.subscriptionParams - Client-facing params.
|
|
1526
|
+
* @returns {Promise<false | {buffered: boolean, ceilingSequence: number, checkpointSequence: number, replaying: boolean} | null>} - Replay state.
|
|
1527
|
+
*/
|
|
1528
|
+
async _prepareReplayState({channel, lastEventId, subscriptionChannel, subscriptionParams}) {
|
|
1529
|
+
if (!lastEventId) return null
|
|
1530
|
+
|
|
1531
|
+
const store = websocketEventLogStoreForConfiguration(this.configuration)
|
|
1532
|
+
const checkpoint = await store.getEventById({channel, id: lastEventId})
|
|
1533
|
+
|
|
1534
|
+
if (!checkpoint) {
|
|
1535
|
+
this.sendJson({channel: subscriptionChannel, lastEventId, params: subscriptionParams, type: "replay-gap"})
|
|
1536
|
+
return false
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
return {
|
|
1540
|
+
buffered: false,
|
|
1541
|
+
ceilingSequence: (await store.latestSequence(channel)) || checkpoint.sequence,
|
|
1542
|
+
checkpointSequence: checkpoint.sequence,
|
|
1543
|
+
replaying: true
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* Runs replay channel events.
|
|
1549
|
+
* @param {object} args - Options.
|
|
1550
|
+
* @param {string} args.channel - Channel name.
|
|
1551
|
+
* @param {{buffered: boolean, ceilingSequence: number, checkpointSequence: number, replaying: boolean}} args.replayState - Replay state.
|
|
1552
|
+
* @returns {Promise<void>} - Resolves when replay completes.
|
|
1553
|
+
*/
|
|
1554
|
+
async _replayChannelEvents({channel, replayState}) {
|
|
1555
|
+
const store = websocketEventLogStoreForConfiguration(this.configuration)
|
|
1556
|
+
const events = await store.getEventsAfter({
|
|
1557
|
+
channel,
|
|
1558
|
+
sequence: replayState.checkpointSequence,
|
|
1559
|
+
upToSequence: replayState.ceilingSequence
|
|
1560
|
+
})
|
|
1561
|
+
|
|
1562
|
+
for (const event of events) {
|
|
1563
|
+
await this.sendEvent(channel, event.payload, {
|
|
1564
|
+
createdAt: event.createdAt,
|
|
1565
|
+
eventId: event.id,
|
|
1566
|
+
replayed: true,
|
|
1567
|
+
sequence: event.sequence
|
|
1568
|
+
})
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* Runs finish replay state.
|
|
1574
|
+
* @param {string} channel - Channel name.
|
|
1575
|
+
* @param {{buffered: boolean, ceilingSequence: number, checkpointSequence: number, replaying: boolean}} replayState - Replay state.
|
|
1576
|
+
* @returns {Promise<void>} - Resolves when buffered events are flushed.
|
|
1577
|
+
*/
|
|
1578
|
+
async _finishReplayState(channel, replayState) {
|
|
1579
|
+
const store = websocketEventLogStoreForConfiguration(this.configuration)
|
|
1580
|
+
|
|
1581
|
+
replayState.replaying = false
|
|
1582
|
+
this.channelReplayStates.delete(channel)
|
|
1583
|
+
|
|
1584
|
+
if (!replayState.buffered) return
|
|
1585
|
+
|
|
1586
|
+
const liveEvents = await store.getEventsAfter({
|
|
1587
|
+
channel,
|
|
1588
|
+
sequence: replayState.ceilingSequence
|
|
1589
|
+
})
|
|
1590
|
+
|
|
1591
|
+
for (const event of liveEvents) {
|
|
1592
|
+
await this.sendEvent(channel, event.payload, {
|
|
1593
|
+
createdAt: event.createdAt,
|
|
1594
|
+
eventId: event.id,
|
|
1595
|
+
sequence: event.sequence
|
|
1596
|
+
})
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
/**
|
|
1601
|
+
* Runs resolve tenant.
|
|
1602
|
+
* @param {{channel?: string, params?: Record<string, ?>}} args - Tenant resolution args.
|
|
1603
|
+
* @returns {Promise<string | null | undefined>} - Resolved tenant.
|
|
1604
|
+
*/
|
|
1605
|
+
async _resolveTenant({channel, params}) {
|
|
1606
|
+
const requestParams = this.upgradeRequest?.params?.()
|
|
1607
|
+
const mergedParams = {
|
|
1608
|
+
...(requestParams && typeof requestParams === "object" ? requestParams : {}),
|
|
1609
|
+
...(params && typeof params === "object" ? params : {})
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
return /** Narrows the runtime value to the documented type. @type {Promise<string | null | undefined>} */ (this.configuration.resolveTenant({
|
|
1613
|
+
params: mergedParams,
|
|
1614
|
+
request: this.upgradeRequest,
|
|
1615
|
+
response: undefined,
|
|
1616
|
+
subscription: channel ? {channel, params} : undefined
|
|
1617
|
+
}))
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
/**
|
|
1621
|
+
* Runs unmask payload.
|
|
1622
|
+
* @param {Buffer} payload - Payload data.
|
|
1623
|
+
* @param {Buffer} mask - Mask.
|
|
1624
|
+
* @returns {Buffer} - The unmask payload.
|
|
1625
|
+
*/
|
|
1626
|
+
_unmaskPayload(payload, mask) {
|
|
1627
|
+
/**
|
|
1628
|
+
* Result.
|
|
1629
|
+
@type {Buffer} */
|
|
1630
|
+
const result = Buffer.alloc(payload.length)
|
|
1631
|
+
|
|
1632
|
+
for (let i = 0; i < payload.length; i++) {
|
|
1633
|
+
result[i] = payload[i] ^ mask[i % 4]
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
return result
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
async _runMessageHandlerOpen() {
|
|
1640
|
+
try {
|
|
1641
|
+
const handler = this.messageHandler
|
|
1642
|
+
const onOpen = handler ? handler.onOpen : null
|
|
1643
|
+
|
|
1644
|
+
if (onOpen) {
|
|
1645
|
+
await onOpen({session: this})
|
|
1646
|
+
}
|
|
1647
|
+
} catch (error) {
|
|
1648
|
+
this.logger.error(() => ["Websocket open handler failed", error])
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
/**
|
|
1653
|
+
* Runs run message handler message.
|
|
1654
|
+
* @param {WebsocketSessionMessage} message - Incoming websocket message.
|
|
1655
|
+
* @returns {Promise<void>} - Resolves when complete.
|
|
1656
|
+
*/
|
|
1657
|
+
async _runMessageHandlerMessage(message) {
|
|
1658
|
+
try {
|
|
1659
|
+
const handler = this.messageHandler
|
|
1660
|
+
const onMessage = handler ? handler.onMessage : null
|
|
1661
|
+
|
|
1662
|
+
if (onMessage) {
|
|
1663
|
+
await onMessage({message, session: this})
|
|
1664
|
+
}
|
|
1665
|
+
} catch (error) {
|
|
1666
|
+
this.logger.error(() => ["Websocket message handler failed", error])
|
|
1667
|
+
const handler = this.messageHandler
|
|
1668
|
+
const onError = handler ? handler.onError : null
|
|
1669
|
+
|
|
1670
|
+
if (onError) {
|
|
1671
|
+
await onError({error: error instanceof Error ? error : new Error(String(error)), session: this})
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
async _runMessageHandlerClose() {
|
|
1677
|
+
try {
|
|
1678
|
+
const handler = this.messageHandler
|
|
1679
|
+
const onClose = handler ? handler.onClose : null
|
|
1680
|
+
|
|
1681
|
+
if (onClose) {
|
|
1682
|
+
await onClose({session: this})
|
|
1683
|
+
}
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
this.logger.error(() => ["Websocket close handler failed", error])
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* Runs remote address.
|
|
1691
|
+
* @returns {string | undefined} - Remote address resolved from the websocket upgrade request.
|
|
1692
|
+
*/
|
|
1693
|
+
remoteAddress() {
|
|
1694
|
+
return this.upgradeRequest?.remoteAddress() || this.client.remoteAddress
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
/**
|
|
1698
|
+
* Runs set message handler.
|
|
1699
|
+
* @param {import("../../configuration-types.js").WebsocketMessageHandler} handler - Handler instance.
|
|
1700
|
+
* @returns {void}
|
|
1701
|
+
*/
|
|
1702
|
+
setMessageHandler(handler) {
|
|
1703
|
+
this.messageHandler = handler
|
|
1704
|
+
void this._runMessageHandlerOpen()
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
async _resolveMessageHandlerPromise() {
|
|
1708
|
+
if (!this.messageHandlerPromise) return
|
|
1709
|
+
|
|
1710
|
+
try {
|
|
1711
|
+
const handler = await this.messageHandlerPromise
|
|
1712
|
+
|
|
1713
|
+
if (handler) {
|
|
1714
|
+
this.pendingMessageHandler = false
|
|
1715
|
+
this.messageHandlerPromise = undefined
|
|
1716
|
+
// Install handler and drain onOpen before replaying queued
|
|
1717
|
+
// messages. setMessageHandler() fires onOpen as fire-and-forget;
|
|
1718
|
+
// awaiting _runMessageHandlerOpen() directly here closes the
|
|
1719
|
+
// race where queued subscribe/connection-* frames would
|
|
1720
|
+
// dispatch while an async onOpen is still setting up session
|
|
1721
|
+
// state.
|
|
1722
|
+
this.messageHandler = handler
|
|
1723
|
+
await this._runMessageHandlerOpen()
|
|
1724
|
+
await this._flushQueuedMessages({useHandler: typeof handler.onMessage === "function"})
|
|
1725
|
+
return
|
|
1726
|
+
}
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
this.logger.error(() => ["Websocket message handler resolver failed", error])
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
this.pendingMessageHandler = false
|
|
1732
|
+
this.messageHandlerPromise = undefined
|
|
1733
|
+
await this._flushQueuedMessages({useHandler: false})
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
/**
|
|
1737
|
+
* Runs flush queued messages.
|
|
1738
|
+
* @param {{useHandler: boolean}} args - Args.
|
|
1739
|
+
* @returns {Promise<void>} - Resolves when complete.
|
|
1740
|
+
*/
|
|
1741
|
+
async _flushQueuedMessages({useHandler}) {
|
|
1742
|
+
if (this.messageQueue.length === 0) return
|
|
1743
|
+
|
|
1744
|
+
const queued = this.messageQueue.slice()
|
|
1745
|
+
this.messageQueue = []
|
|
1746
|
+
|
|
1747
|
+
for (const message of queued) {
|
|
1748
|
+
if (useHandler && this.messageHandler) {
|
|
1749
|
+
await this._runMessageHandlerMessage(message)
|
|
1750
|
+
} else {
|
|
1751
|
+
await this._handleMessage(message)
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
}
|