@ossy/platform 1.24.0 → 1.24.1
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/package.json +5 -4
- package/src/index.js +1 -0
- package/src/server.js +18 -0
- package/src/tasks/change-stream.js +134 -0
- package/src/tasks/task-service.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/platform",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.1",
|
|
4
4
|
"description": "Ossy application server runtime",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,16 +23,17 @@
|
|
|
23
23
|
"author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@ossy/router": "^1.25.
|
|
27
|
-
"@ossy/sdk": "^1.25.
|
|
26
|
+
"@ossy/router": "^1.25.1",
|
|
27
|
+
"@ossy/sdk": "^1.25.1",
|
|
28
28
|
"cookie-parser": "^1.4.7",
|
|
29
29
|
"dotenv": ">=16.0.0 <18.0.0",
|
|
30
30
|
"express": ">=5.0.0 <6.0.0",
|
|
31
|
+
"mongodb": "^7.2.0",
|
|
31
32
|
"morgan": ">=1.10.1 <2.0.0"
|
|
32
33
|
},
|
|
33
34
|
"files": [
|
|
34
35
|
"src",
|
|
35
36
|
"Dockerfile"
|
|
36
37
|
],
|
|
37
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "c8a5e5af59c1975ea8e0c5a974202c6c146236c3"
|
|
38
39
|
}
|
package/src/index.js
CHANGED
package/src/server.js
CHANGED
|
@@ -6,6 +6,8 @@ import morgan from 'morgan'
|
|
|
6
6
|
import { Router as OssyRouter } from '@ossy/router'
|
|
7
7
|
import cookieParser from 'cookie-parser'
|
|
8
8
|
import { ProxyInternal } from './proxy-internal.js'
|
|
9
|
+
import { TaskService } from './tasks/task-service.js'
|
|
10
|
+
import { ChangeStream } from './tasks/change-stream.js'
|
|
9
11
|
|
|
10
12
|
const DEFAULT_PORT = 3000
|
|
11
13
|
const MANIFEST_FILE = 'manifest.json'
|
|
@@ -87,6 +89,22 @@ export async function startServer (options = {}) {
|
|
|
87
89
|
const manifest = loadManifest(buildDir)
|
|
88
90
|
const config = manifest.config
|
|
89
91
|
|
|
92
|
+
for (const task of manifest.tasks ?? []) {
|
|
93
|
+
try {
|
|
94
|
+
const mod = await import(resolveEntryUrl(task.entry, buildDir))
|
|
95
|
+
if (mod.metadata) {
|
|
96
|
+
TaskService.registerTask(mod)
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error(`[@ossy/platform][server] Failed to load task "${task.id}":`, err.message)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
TaskService.startScheduler()
|
|
103
|
+
|
|
104
|
+
if (process.env.DB_URL) {
|
|
105
|
+
ChangeStream.start(process.env.DB_URL)
|
|
106
|
+
}
|
|
107
|
+
|
|
90
108
|
const supportedLanguages = Array.isArray(config.supportedLanguages) ? config.supportedLanguages : []
|
|
91
109
|
const defaultLanguage = config.defaultLanguage
|
|
92
110
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { TaskService } from './task-service.js'
|
|
2
|
+
|
|
3
|
+
function isMongoTopologyClosedError(error) {
|
|
4
|
+
if (!error || typeof error !== 'object') return false
|
|
5
|
+
if (error.name === 'MongoTopologyClosedError') return true
|
|
6
|
+
if (typeof error.message === 'string' && error.message.includes('Topology is closed')) return true
|
|
7
|
+
return false
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ChangeStream {
|
|
11
|
+
|
|
12
|
+
static _stopped = false
|
|
13
|
+
static _reconnectAttempts = 0
|
|
14
|
+
static _dbUrl = null
|
|
15
|
+
static _client = null
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Opens the MongoDB changestream and wires up reconnect logic.
|
|
19
|
+
* Logs errors; does not crash the process on Mongo outages.
|
|
20
|
+
* @param {string} dbUrl - MongoDB connection URL (process.env.DB_URL)
|
|
21
|
+
*/
|
|
22
|
+
static start(dbUrl) {
|
|
23
|
+
ChangeStream._stopped = false
|
|
24
|
+
ChangeStream._reconnectAttempts = 0
|
|
25
|
+
ChangeStream._dbUrl = dbUrl
|
|
26
|
+
ChangeStream._open().catch((error) => {
|
|
27
|
+
console.error('[ChangeStream] Change stream could not be opened', error)
|
|
28
|
+
ChangeStream._scheduleReconnect()
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static stop() {
|
|
33
|
+
ChangeStream._stopped = true
|
|
34
|
+
if (ChangeStream._client) {
|
|
35
|
+
ChangeStream._client.close().catch(() => {})
|
|
36
|
+
ChangeStream._client = null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static async _getMongoClient() {
|
|
41
|
+
const { MongoClient } = await import('mongodb')
|
|
42
|
+
return MongoClient
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static async _getClient() {
|
|
46
|
+
if (!ChangeStream._client) {
|
|
47
|
+
const MongoClient = await ChangeStream._getMongoClient()
|
|
48
|
+
ChangeStream._client = new MongoClient(ChangeStream._dbUrl, {
|
|
49
|
+
serverSelectionTimeoutMS: 30_000,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
return ChangeStream._client
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static async _resetClient() {
|
|
56
|
+
const MongoClient = await ChangeStream._getMongoClient()
|
|
57
|
+
const prev = ChangeStream._client
|
|
58
|
+
ChangeStream._client = new MongoClient(ChangeStream._dbUrl, {
|
|
59
|
+
serverSelectionTimeoutMS: 30_000,
|
|
60
|
+
})
|
|
61
|
+
if (prev) {
|
|
62
|
+
prev.close().catch(() => {})
|
|
63
|
+
}
|
|
64
|
+
console.log('[ChangeStream] New MongoClient instance created')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static _scheduleReconnect() {
|
|
68
|
+
if (ChangeStream._stopped) return
|
|
69
|
+
|
|
70
|
+
const delaySecs = Math.min(Math.pow(2, ChangeStream._reconnectAttempts), 30)
|
|
71
|
+
ChangeStream._reconnectAttempts++
|
|
72
|
+
|
|
73
|
+
console.log(`[ChangeStream] Reconnecting in ${delaySecs}s...`)
|
|
74
|
+
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
if (ChangeStream._stopped) return
|
|
77
|
+
ChangeStream._open().catch((error) => {
|
|
78
|
+
console.error('[ChangeStream] Change stream could not be opened', error)
|
|
79
|
+
ChangeStream._scheduleReconnect()
|
|
80
|
+
})
|
|
81
|
+
}, delaySecs * 1000)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static async _open() {
|
|
85
|
+
console.log('[ChangeStream] Watching for changes')
|
|
86
|
+
|
|
87
|
+
const dbName = process.env.DB_NAME || 'test'
|
|
88
|
+
const client = await ChangeStream._getClient()
|
|
89
|
+
const collection = client.db(dbName).collection('eventstore')
|
|
90
|
+
|
|
91
|
+
const changeStream = collection.watch([
|
|
92
|
+
{ $match: { operationType: 'insert' } },
|
|
93
|
+
])
|
|
94
|
+
|
|
95
|
+
const wasReconnecting = ChangeStream._reconnectAttempts > 0
|
|
96
|
+
ChangeStream._reconnectAttempts = 0
|
|
97
|
+
if (wasReconnecting) {
|
|
98
|
+
console.log('[ChangeStream] Reconnected.')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Ensures only one reconnect is scheduled if both 'error' and 'close' fire for the same failure.
|
|
102
|
+
let reconnectScheduled = false
|
|
103
|
+
const scheduleOnce = () => {
|
|
104
|
+
if (!reconnectScheduled) {
|
|
105
|
+
reconnectScheduled = true
|
|
106
|
+
ChangeStream._scheduleReconnect()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
changeStream.on('error', (error) => {
|
|
111
|
+
console.error('[ChangeStream] Change stream error (server keeps running)', error)
|
|
112
|
+
if (isMongoTopologyClosedError(error)) {
|
|
113
|
+
ChangeStream._resetClient().catch(() => {})
|
|
114
|
+
}
|
|
115
|
+
scheduleOnce()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
changeStream.on('change', (change) => {
|
|
119
|
+
console.log('[ChangeStream] Change detected')
|
|
120
|
+
TaskService.dispatch(change.fullDocument)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
changeStream.on('close', () => {
|
|
124
|
+
console.log('[ChangeStream] close detected')
|
|
125
|
+
scheduleOnce()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
changeStream.on('end', () => {
|
|
129
|
+
console.log('[ChangeStream] end detected')
|
|
130
|
+
scheduleOnce()
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
}
|
|
@@ -19,11 +19,11 @@ export class TaskService {
|
|
|
19
19
|
* @param {{ metadata: { id: string, triggers?: Array, schedule?: string }, default: function }} taskModule
|
|
20
20
|
*/
|
|
21
21
|
static registerTask(taskModule) {
|
|
22
|
-
const handler = taskModule.
|
|
22
|
+
const handler = taskModule.run
|
|
23
23
|
const metadata = taskModule.metadata
|
|
24
24
|
|
|
25
25
|
if (typeof handler !== 'function') {
|
|
26
|
-
console.error(`[TaskService] Task "${metadata?.id}" has no
|
|
26
|
+
console.error(`[TaskService] Task "${metadata?.id}" has no exported "run" function — skipping`)
|
|
27
27
|
return
|
|
28
28
|
}
|
|
29
29
|
if (!metadata?.id) {
|