chore: publish pearcord-scheduled-messages from pearcord workspace
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
# pearcord-scheduled-messages
|
||||||
|
|
||||||
|
Guild + DM scheduled message queue persistence for Pearcord.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Phase 674 module wrapping HyperDB `@pearcord/scheduled-messages` with JsonStore fallback. Metadata is gossiped on the guild mesh; message body is sent only at fire time.
|
||||||
|
|
||||||
|
## Exports
|
||||||
|
|
||||||
|
| Symbol | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `ScheduledMessageStore` | Queue CRUD, capacity checks, list by guild/channel |
|
||||||
|
|
||||||
|
## Policy
|
||||||
|
|
||||||
|
Uses `pearcord-shared/dm-scheduled-policy` for recurrence computation and queue caps (per channel / per guild).
|
||||||
|
|
||||||
|
## Platform integration
|
||||||
|
|
||||||
|
- `pearcord-platform/polls-scheduling.js` — `scheduleMessage`, `updateScheduledMessage`, `cancelScheduledMessage`, `sendNowScheduledMessage`, due runner, gossip + heal
|
||||||
|
- View fields: `scheduledQueue`, `scheduledQueueCount`
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:phase674-polls-scheduling
|
||||||
|
PEARCORD_SKIP_MESH_ROUNDTRIP=1 npm run test:agentctl-phase674-polls-scheduling
|
||||||
|
```
|
||||||
|
|
||||||
|
See [SCHEDULED_MESSAGES.md](../../docs/SCHEDULED_MESSAGES.md).
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const path = require('bare-path')
|
||||||
|
const { JsonStore } = require('pearcord-db/store-json')
|
||||||
|
const { COLLECTIONS } = require('pearcord-shared')
|
||||||
|
const {
|
||||||
|
DM_SCHEDULE_MAX_QUEUED_PER_CHANNEL,
|
||||||
|
DM_SCHEDULE_STALE_RETENTION_MS
|
||||||
|
} = require('pearcord-shared/dm-scheduled-policy')
|
||||||
|
|
||||||
|
const GUILD_SCHEDULE_MAX_QUEUED_PER_CHANNEL = DM_SCHEDULE_MAX_QUEUED_PER_CHANNEL
|
||||||
|
const GUILD_SCHEDULE_MAX_QUEUED_PER_GUILD = 100
|
||||||
|
|
||||||
|
class ScheduledMessageStore {
|
||||||
|
constructor (opts = {}) {
|
||||||
|
this.db = opts.db || null
|
||||||
|
this.storagePath = opts.storagePath || './pearcord-storage'
|
||||||
|
this.collection = COLLECTIONS.SCHEDULED_MESSAGES
|
||||||
|
this._json = opts.jsonStore || new JsonStore(path.join(this.storagePath, 'scheduled-messages'))
|
||||||
|
this._hyperDbReady = false
|
||||||
|
}
|
||||||
|
|
||||||
|
get engine () {
|
||||||
|
return this.db?.getEngine?.() === 'hyperdb' && this._hyperDbReady ? 'hyperdb' : 'json'
|
||||||
|
}
|
||||||
|
|
||||||
|
async ready () {
|
||||||
|
await this._json.ready()
|
||||||
|
if (this.db) {
|
||||||
|
await this.db.ready()
|
||||||
|
if (this.db.getEngine?.() === 'hyperdb') {
|
||||||
|
try {
|
||||||
|
await this.db.get(this.collection, { guildId: '__probe__', id: '__probe__' })
|
||||||
|
this._hyperDbReady = true
|
||||||
|
} catch (err) {
|
||||||
|
if (/Unknown index|Unknown collection/.test(err.message)) this._hyperDbReady = false
|
||||||
|
else this._hyperDbReady = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
_disableHyperDb (err) {
|
||||||
|
if (err && /Unknown index|Unknown collection/.test(err.message)) {
|
||||||
|
this._hyperDbReady = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async upsert (row) {
|
||||||
|
if (!row?.id || !row?.guildId) throw new Error('id and guildId required')
|
||||||
|
const merged = { ...row, updatedAt: Date.now() }
|
||||||
|
if (this.engine === 'hyperdb') {
|
||||||
|
try {
|
||||||
|
await this.db.insert(this.collection, merged)
|
||||||
|
return merged
|
||||||
|
} catch (err) {
|
||||||
|
if (!this._disableHyperDb(err)) throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this._json.insert('@pearcord/scheduled-messages-json', merged)
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
async get (guildId, id) {
|
||||||
|
if (!guildId || !id) return null
|
||||||
|
if (this.engine === 'hyperdb') {
|
||||||
|
try {
|
||||||
|
return await this.db.get(this.collection, { guildId, id })
|
||||||
|
} catch (err) {
|
||||||
|
if (this._disableHyperDb(err)) {
|
||||||
|
return this._json.get('@pearcord/scheduled-messages-json', { guildId, id })
|
||||||
|
}
|
||||||
|
if (/not found/i.test(err.message)) return null
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this._json.get('@pearcord/scheduled-messages-json', { guildId, id })
|
||||||
|
}
|
||||||
|
|
||||||
|
async listByGuild (guildId, { channelId = null, status = 'queued', userId = null } = {}) {
|
||||||
|
if (!guildId) return []
|
||||||
|
let rows = []
|
||||||
|
if (this.engine === 'hyperdb') {
|
||||||
|
try {
|
||||||
|
rows = await this.db.find(this.collection, { guildId })
|
||||||
|
} catch (err) {
|
||||||
|
if (this._disableHyperDb(err)) rows = []
|
||||||
|
else throw err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const raw = await this._json.find('@pearcord/scheduled-messages-json', { guildId })
|
||||||
|
rows = Array.isArray(raw) ? raw : raw ? [raw] : []
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
.filter((r) => !channelId || r.channelId === channelId)
|
||||||
|
.filter((r) => !status || r.status === status)
|
||||||
|
.filter((r) => !userId || r.userId === userId)
|
||||||
|
.sort((a, b) => (a.sendAt || 0) - (b.sendAt || 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
async countQueued ({ guildId, channelId = null }) {
|
||||||
|
const rows = await this.listByGuild(guildId, { channelId, status: 'queued' })
|
||||||
|
return rows.length
|
||||||
|
}
|
||||||
|
|
||||||
|
async assertQueueCapacity ({ guildId, channelId }) {
|
||||||
|
const channelCount = await this.countQueued({ guildId, channelId })
|
||||||
|
if (channelCount >= GUILD_SCHEDULE_MAX_QUEUED_PER_CHANNEL) {
|
||||||
|
throw new Error(`scheduled message queue full (${GUILD_SCHEDULE_MAX_QUEUED_PER_CHANNEL} per channel)`)
|
||||||
|
}
|
||||||
|
const guildCount = await this.countQueued({ guildId })
|
||||||
|
if (guildCount >= GUILD_SCHEDULE_MAX_QUEUED_PER_GUILD) {
|
||||||
|
throw new Error(`scheduled message queue full (${GUILD_SCHEDULE_MAX_QUEUED_PER_GUILD} per guild)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async pruneStale (guildId) {
|
||||||
|
const cutoff = Date.now() - DM_SCHEDULE_STALE_RETENTION_MS
|
||||||
|
const rows = await this.listByGuild(guildId, { status: null })
|
||||||
|
let pruned = 0
|
||||||
|
for (const row of rows) {
|
||||||
|
if (row.status === 'queued') continue
|
||||||
|
const ts = row.sentAt || row.updatedAt || row.createdAt || 0
|
||||||
|
if (ts >= cutoff) continue
|
||||||
|
if (this.engine === 'hyperdb') {
|
||||||
|
await this.db.delete(this.collection, { guildId: row.guildId, id: row.id }).catch(() => {})
|
||||||
|
} else {
|
||||||
|
await this._json.delete('@pearcord/scheduled-messages-json', { guildId: row.guildId, id: row.id })
|
||||||
|
}
|
||||||
|
pruned++
|
||||||
|
}
|
||||||
|
return { pruned }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ScheduledMessageStore,
|
||||||
|
GUILD_SCHEDULE_MAX_QUEUED_PER_CHANNEL,
|
||||||
|
GUILD_SCHEDULE_MAX_QUEUED_PER_GUILD
|
||||||
|
}
|
||||||
Generated
+62
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"name": "pearcord-scheduled-messages",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "pearcord-scheduled-messages",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"bare-path": "^3.0.0",
|
||||||
|
"pearcord-db": "file:../pearcord-db",
|
||||||
|
"pearcord-shared": "file:../pearcord-shared"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../pearcord-db": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"bare-fs": "^4.0.0",
|
||||||
|
"bare-path": "^3.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"hyperdb": "^5.0.0",
|
||||||
|
"hyperschema": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../pearcord-shared": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"b4a": "^1.6.7",
|
||||||
|
"compact-encoding": "^2.0.0",
|
||||||
|
"hypercore-crypto": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-os": {
|
||||||
|
"version": "3.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz",
|
||||||
|
"integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"bare": ">=1.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-path": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"bare-os": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pearcord-db": {
|
||||||
|
"resolved": "../pearcord-db",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/pearcord-shared": {
|
||||||
|
"resolved": "../pearcord-shared",
|
||||||
|
"link": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "pearcord-scheduled-messages",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "commonjs",
|
||||||
|
"description": "Guild + DM scheduled message queue for Pearcord",
|
||||||
|
"dependencies": {
|
||||||
|
"bare-path": "^3.0.0",
|
||||||
|
"pearcord-db": "git+https://git.ssh.surf/pearcord/pearcord-db.git#main",
|
||||||
|
"pearcord-shared": "git+https://git.ssh.surf/pearcord/pearcord-shared.git#main"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user