refactor(server-*): refactor client, better state managment
This commit is contained in:
@@ -9,6 +9,10 @@ import type {
|
|||||||
import type { AuthenticatedPeer, Peer } from './types'
|
import type { AuthenticatedPeer, Peer } from './types'
|
||||||
|
|
||||||
import { availableLogLevelStrings, Format, LogLevelString, logLevelStringToLogLevelMap, useLogg } from '@guiiai/logg'
|
import { availableLogLevelStrings, Format, LogLevelString, logLevelStringToLogLevelMap, useLogg } from '@guiiai/logg'
|
||||||
|
import {
|
||||||
|
createInvalidJsonServerErrorMessage,
|
||||||
|
ServerErrorMessages,
|
||||||
|
} from '@proj-airi/server-shared'
|
||||||
import { MessageHeartbeat, MessageHeartbeatKind, WebSocketEventSource } from '@proj-airi/server-shared/types'
|
import { MessageHeartbeat, MessageHeartbeatKind, WebSocketEventSource } from '@proj-airi/server-shared/types'
|
||||||
import { defineWebSocketHandler, H3 } from 'h3'
|
import { defineWebSocketHandler, H3 } from 'h3'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
@@ -50,7 +54,7 @@ const RESPONSES = {
|
|||||||
}),
|
}),
|
||||||
notAuthenticated: (serverInstanceId: string, parentId?: string) => ({
|
notAuthenticated: (serverInstanceId: string, parentId?: string) => ({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
data: { message: 'not authenticated' },
|
data: { message: ServerErrorMessages.notAuthenticated },
|
||||||
metadata: createServerEventMetadata(serverInstanceId, parentId),
|
metadata: createServerEventMetadata(serverInstanceId, parentId),
|
||||||
}),
|
}),
|
||||||
error: (message: string, serverInstanceId: string, parentId?: string) => ({
|
error: (message: string, serverInstanceId: string, parentId?: string) => ({
|
||||||
@@ -281,7 +285,7 @@ export function setupApp(options?: AppOptions): { app: H3, closeAllPeers: () =>
|
|||||||
: JSON.parse(text)
|
: JSON.parse(text)
|
||||||
|
|
||||||
if (!potentialEvent || typeof potentialEvent !== 'object' || !('type' in potentialEvent)) {
|
if (!potentialEvent || typeof potentialEvent !== 'object' || !('type' in potentialEvent)) {
|
||||||
send(peer, RESPONSES.error('invalid event format', instanceId))
|
send(peer, RESPONSES.error(ServerErrorMessages.invalidEventFormat, instanceId))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +293,7 @@ export function setupApp(options?: AppOptions): { app: H3, closeAllPeers: () =>
|
|||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : String(err)
|
const errorMessage = err instanceof Error ? err.message : String(err)
|
||||||
send(peer, RESPONSES.error(`invalid JSON, error: ${errorMessage}`, instanceId))
|
send(peer, RESPONSES.error(createInvalidJsonServerErrorMessage(errorMessage), instanceId))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -337,7 +341,7 @@ export function setupApp(options?: AppOptions): { app: H3, closeAllPeers: () =>
|
|||||||
case 'module:authenticate': {
|
case 'module:authenticate': {
|
||||||
if (authToken && event.data.token !== authToken) {
|
if (authToken && event.data.token !== authToken) {
|
||||||
logger.withFields({ peer: peer.id, peerRemote: peer.remoteAddress, peerRequest: peer.request.url }).log('authentication failed')
|
logger.withFields({ peer: peer.id, peerRemote: peer.remoteAddress, peerRequest: peer.request.url }).log('authentication failed')
|
||||||
send(peer, RESPONSES.error('invalid token', instanceId, event.metadata?.event.id))
|
send(peer, RESPONSES.error(ServerErrorMessages.invalidToken, instanceId, event.metadata?.event.id))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -364,24 +368,24 @@ export function setupApp(options?: AppOptions): { app: H3, closeAllPeers: () =>
|
|||||||
// verify
|
// verify
|
||||||
const { name, index, identity } = event.data as { name: string, index?: number, identity?: MetadataEventSource }
|
const { name, index, identity } = event.data as { name: string, index?: number, identity?: MetadataEventSource }
|
||||||
if (!name || typeof name !== 'string') {
|
if (!name || typeof name !== 'string') {
|
||||||
send(peer, RESPONSES.error('the field \'name\' must be a non-empty string for event \'module:announce\'', instanceId))
|
send(peer, RESPONSES.error(ServerErrorMessages.moduleAnnounceNameInvalid, instanceId))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (typeof index !== 'undefined') {
|
if (typeof index !== 'undefined') {
|
||||||
if (!Number.isInteger(index) || index < 0) {
|
if (!Number.isInteger(index) || index < 0) {
|
||||||
send(peer, RESPONSES.error('the field \'index\' must be a non-negative integer for event \'module:announce\'', instanceId))
|
send(peer, RESPONSES.error(ServerErrorMessages.moduleAnnounceIndexInvalid, instanceId))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!identity || identity.kind !== 'plugin' || !identity.plugin?.id) {
|
if (!identity || identity.kind !== 'plugin' || !identity.plugin?.id) {
|
||||||
send(peer, RESPONSES.error('module identity must include kind=plugin and a plugin id for event \'module:announce\'', instanceId))
|
send(peer, RESPONSES.error(ServerErrorMessages.moduleAnnounceIdentityInvalid, instanceId))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (authToken && !p.authenticated) {
|
if (authToken && !p.authenticated) {
|
||||||
send(peer, RESPONSES.error('must authenticate before announcing', instanceId))
|
send(peer, RESPONSES.error(ServerErrorMessages.mustAuthenticateBeforeAnnouncing, instanceId))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -420,13 +424,13 @@ export function setupApp(options?: AppOptions): { app: H3, closeAllPeers: () =>
|
|||||||
const config = data.config
|
const config = data.config
|
||||||
|
|
||||||
if (moduleName === '') {
|
if (moduleName === '') {
|
||||||
send(peer, RESPONSES.error('the field \'moduleName\' can\'t be empty for event \'ui:configure\'', instanceId))
|
send(peer, RESPONSES.error(ServerErrorMessages.uiConfigureModuleNameInvalid, instanceId))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (typeof moduleIndex !== 'undefined') {
|
if (typeof moduleIndex !== 'undefined') {
|
||||||
if (!Number.isInteger(moduleIndex) || moduleIndex < 0) {
|
if (!Number.isInteger(moduleIndex) || moduleIndex < 0) {
|
||||||
send(peer, RESPONSES.error('the field \'moduleIndex\' must be a non-negative integer for event \'ui:configure\'', instanceId))
|
send(peer, RESPONSES.error(ServerErrorMessages.uiConfigureModuleIndexInvalid, instanceId))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -442,7 +446,7 @@ export function setupApp(options?: AppOptions): { app: H3, closeAllPeers: () =>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
send(peer, RESPONSES.error('module not found, it hasn\'t announced itself or the name is incorrect', instanceId))
|
send(peer, RESPONSES.error(ServerErrorMessages.moduleNotFound, instanceId))
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -4,8 +4,17 @@
|
|||||||
"lib": [
|
"lib": [
|
||||||
"ESNext"
|
"ESNext"
|
||||||
],
|
],
|
||||||
|
"baseUrl": ".",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
|
"paths": {
|
||||||
|
"@proj-airi/server-shared": [
|
||||||
|
"../server-shared/src/index.ts"
|
||||||
|
],
|
||||||
|
"@proj-airi/server-shared/*": [
|
||||||
|
"../server-shared/src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
|||||||
@@ -14,9 +14,32 @@ npm i @proj-airi/server-sdk -D
|
|||||||
```typescript
|
```typescript
|
||||||
import { Client } from '@proj-airi/server-sdk'
|
import { Client } from '@proj-airi/server-sdk'
|
||||||
|
|
||||||
const c = new Client({ name: 'your airi plugin' })
|
const client = new Client({
|
||||||
|
name: 'your airi plugin',
|
||||||
|
autoConnect: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
await client.connect()
|
||||||
|
|
||||||
|
client.onEvent('input:text', async (event) => {
|
||||||
|
console.info(event.data.text)
|
||||||
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`connect()` now resolves when the client is fully ready for use, not just when the websocket transport has opened. In practice that means:
|
||||||
|
|
||||||
|
- the socket is open
|
||||||
|
- authentication succeeded when a token is configured
|
||||||
|
- the module has announced itself successfully
|
||||||
|
|
||||||
|
Useful runtime helpers:
|
||||||
|
|
||||||
|
- `client.connectionStatus` exposes the current lifecycle state
|
||||||
|
- `client.isReady` tells you whether the client has completed authentication + announce
|
||||||
|
- `client.send()` returns `false` instead of silently dropping messages when the socket is unavailable
|
||||||
|
- `client.sendOrThrow()` is available when you want strict delivery semantics
|
||||||
|
- `client.onEvent()` returns an unsubscribe function
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[MIT](../../LICENSE)
|
[MIT](../../LICENSE)
|
||||||
|
|||||||
@@ -37,11 +37,9 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@moeru/std": "catalog:",
|
||||||
"@proj-airi/server-shared": "workspace:^",
|
"@proj-airi/server-shared": "workspace:^",
|
||||||
"crossws": "^0.4.4",
|
"crossws": "^0.4.4",
|
||||||
"superjson": "catalog:"
|
"superjson": "catalog:"
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@moeru/std": "catalog:"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+626
-258
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,256 @@
|
|||||||
|
import type { WebSocketEvent } from '@proj-airi/server-shared/types'
|
||||||
|
|
||||||
|
import superjson from 'superjson'
|
||||||
|
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
class MockWebSocket {
|
||||||
|
static readonly CONNECTING = 0
|
||||||
|
static readonly OPEN = 1
|
||||||
|
static readonly CLOSING = 2
|
||||||
|
static readonly CLOSED = 3
|
||||||
|
|
||||||
|
static instances: MockWebSocket[] = []
|
||||||
|
|
||||||
|
readonly sent: string[] = []
|
||||||
|
readyState = MockWebSocket.CONNECTING
|
||||||
|
onclose?: () => void
|
||||||
|
onerror?: (event: { error?: Error }) => void
|
||||||
|
onmessage?: (event: { data: string }) => void
|
||||||
|
onopen?: () => void
|
||||||
|
|
||||||
|
constructor(public readonly url: string) {
|
||||||
|
MockWebSocket.instances.push(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data: string) {
|
||||||
|
this.sent.push(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.readyState = MockWebSocket.CLOSED
|
||||||
|
this.onclose?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
ping() {}
|
||||||
|
pong() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock('crossws/websocket', () => ({
|
||||||
|
default: MockWebSocket,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const { Client } = await import('../src/client')
|
||||||
|
|
||||||
|
function lastSocket() {
|
||||||
|
const socket = MockWebSocket.instances.at(-1)
|
||||||
|
if (!socket) {
|
||||||
|
throw new Error('No mock websocket instance created')
|
||||||
|
}
|
||||||
|
|
||||||
|
return socket
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSent(socket: MockWebSocket, index = -1) {
|
||||||
|
const payload = socket.sent.at(index)
|
||||||
|
if (!payload) {
|
||||||
|
throw new Error(`No sent payload at index ${index}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return superjson.parse<WebSocketEvent>(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitOpen(socket: MockWebSocket) {
|
||||||
|
socket.readyState = MockWebSocket.OPEN
|
||||||
|
socket.onopen?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitMessage(socket: MockWebSocket, event: WebSocketEvent) {
|
||||||
|
socket.onmessage?.({
|
||||||
|
data: superjson.stringify(event),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
MockWebSocket.instances.length = 0
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('client', () => {
|
||||||
|
it('resolves connect only after authentication and self announcement', async () => {
|
||||||
|
const client = new Client({
|
||||||
|
autoConnect: false,
|
||||||
|
autoReconnect: false,
|
||||||
|
name: 'test-plugin',
|
||||||
|
token: 'secret',
|
||||||
|
})
|
||||||
|
|
||||||
|
const connected = client.connect()
|
||||||
|
const socket = lastSocket()
|
||||||
|
|
||||||
|
emitOpen(socket)
|
||||||
|
|
||||||
|
expect(parseSent(socket)).toMatchObject({
|
||||||
|
type: 'module:authenticate',
|
||||||
|
data: { token: 'secret' },
|
||||||
|
})
|
||||||
|
|
||||||
|
emitMessage(socket, {
|
||||||
|
type: 'module:authenticated',
|
||||||
|
data: { authenticated: true },
|
||||||
|
metadata: {
|
||||||
|
source: { kind: 'plugin', plugin: { id: 'server' }, id: 'server-1' },
|
||||||
|
event: { id: 'auth-1' },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const announceEvent = parseSent(socket)
|
||||||
|
expect(announceEvent).toMatchObject({
|
||||||
|
type: 'module:announce',
|
||||||
|
data: { name: 'test-plugin' },
|
||||||
|
})
|
||||||
|
|
||||||
|
emitMessage(socket, {
|
||||||
|
type: 'module:announced',
|
||||||
|
data: {
|
||||||
|
name: 'test-plugin',
|
||||||
|
identity: announceEvent.data.identity,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
source: { kind: 'plugin', plugin: { id: 'server' }, id: 'server-1' },
|
||||||
|
event: { id: 'announce-1' },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(connected).resolves.toBeUndefined()
|
||||||
|
expect(client.connectionStatus).toBe('ready')
|
||||||
|
expect(client.isReady).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fails terminally on invalid token', async () => {
|
||||||
|
const client = new Client({
|
||||||
|
autoConnect: false,
|
||||||
|
autoReconnect: true,
|
||||||
|
name: 'test-plugin',
|
||||||
|
token: 'wrong-token',
|
||||||
|
})
|
||||||
|
|
||||||
|
const connected = client.connect()
|
||||||
|
const socket = lastSocket()
|
||||||
|
|
||||||
|
emitOpen(socket)
|
||||||
|
emitMessage(socket, {
|
||||||
|
type: 'error',
|
||||||
|
data: { message: 'invalid token' },
|
||||||
|
metadata: {
|
||||||
|
source: { kind: 'plugin', plugin: { id: 'server' }, id: 'server-1' },
|
||||||
|
event: { id: 'error-1' },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(connected).rejects.toThrow('invalid token')
|
||||||
|
expect(client.connectionStatus).toBe('failed')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns an unsubscribe function from onEvent', () => {
|
||||||
|
const client = new Client({
|
||||||
|
autoConnect: false,
|
||||||
|
autoReconnect: false,
|
||||||
|
name: 'test-plugin',
|
||||||
|
})
|
||||||
|
|
||||||
|
const listener = vi.fn()
|
||||||
|
const dispose = client.onEvent('input:text', listener)
|
||||||
|
|
||||||
|
dispose()
|
||||||
|
expect(() => client.offEvent('input:text', listener)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('supports timeout-aware ensureConnected without cancelling the shared connect task', async () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
autoConnect: false,
|
||||||
|
autoReconnect: false,
|
||||||
|
name: 'test-plugin',
|
||||||
|
})
|
||||||
|
|
||||||
|
const timedOut = client.ensureConnected({ timeout: 50 })
|
||||||
|
const timedOutAssertion = expect(timedOut).rejects.toThrow('Connection timed out after 50ms')
|
||||||
|
const socket = lastSocket()
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(50)
|
||||||
|
await timedOutAssertion
|
||||||
|
|
||||||
|
emitOpen(socket)
|
||||||
|
const announceEvent = parseSent(socket)
|
||||||
|
|
||||||
|
emitMessage(socket, {
|
||||||
|
type: 'module:announced',
|
||||||
|
data: {
|
||||||
|
name: 'test-plugin',
|
||||||
|
identity: announceEvent.data.identity,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
source: { kind: 'plugin', plugin: { id: 'server' }, id: 'server-1' },
|
||||||
|
event: { id: 'announce-1' },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(client.ensureConnected()).resolves.toBeUndefined()
|
||||||
|
expect(client.isReady).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('supports abort-aware connect', async () => {
|
||||||
|
const client = new Client({
|
||||||
|
autoConnect: false,
|
||||||
|
autoReconnect: false,
|
||||||
|
name: 'test-plugin',
|
||||||
|
})
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
const connecting = client.connect({ abortSignal: controller.signal })
|
||||||
|
|
||||||
|
lastSocket()
|
||||||
|
controller.abort()
|
||||||
|
|
||||||
|
await expect(connecting).rejects.toThrow('Connection aborted')
|
||||||
|
expect(client.connectionStatus).toBe('connecting')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('notifies external state listeners', async () => {
|
||||||
|
const client = new Client({
|
||||||
|
autoConnect: false,
|
||||||
|
autoReconnect: false,
|
||||||
|
name: 'test-plugin',
|
||||||
|
})
|
||||||
|
|
||||||
|
const listener = vi.fn()
|
||||||
|
const dispose = client.onConnectionStateChange(listener)
|
||||||
|
const connected = client.connect()
|
||||||
|
const socket = lastSocket()
|
||||||
|
|
||||||
|
emitOpen(socket)
|
||||||
|
|
||||||
|
const announceEvent = parseSent(socket)
|
||||||
|
emitMessage(socket, {
|
||||||
|
type: 'module:announced',
|
||||||
|
data: {
|
||||||
|
name: 'test-plugin',
|
||||||
|
identity: announceEvent.data.identity,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
source: { kind: 'plugin', plugin: { id: 'server' }, id: 'server-1' },
|
||||||
|
event: { id: 'announce-1' },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await connected
|
||||||
|
|
||||||
|
expect(listener).toHaveBeenCalledWith({ previousStatus: 'idle', status: 'connecting' })
|
||||||
|
expect(listener).toHaveBeenCalledWith({ previousStatus: 'connecting', status: 'announcing' })
|
||||||
|
expect(listener).toHaveBeenCalledWith({ previousStatus: 'announcing', status: 'ready' })
|
||||||
|
|
||||||
|
dispose()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
root: import.meta.dirname,
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
include: ['test/**/*.test.ts'],
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -15,13 +15,17 @@
|
|||||||
"directory": "packages/server-shared"
|
"directory": "packages/server-shared"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.mts",
|
||||||
|
"default": "./dist/index.mjs"
|
||||||
|
},
|
||||||
"./types": {
|
"./types": {
|
||||||
"types": "./dist/types/index.d.mts",
|
"types": "./dist/types/index.d.mts",
|
||||||
"default": "./dist/types/index.mjs"
|
"default": "./dist/types/index.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"main": "./dist/types/index.mjs",
|
"main": "./dist/index.mjs",
|
||||||
"types": "./dist/types/index.d.mts",
|
"types": "./dist/index.d.mts",
|
||||||
"files": [
|
"files": [
|
||||||
"README.md",
|
"README.md",
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
export const ServerErrorMessages = {
|
||||||
|
invalidEventFormat: 'invalid event format',
|
||||||
|
invalidToken: 'invalid token',
|
||||||
|
mustAuthenticateBeforeAnnouncing: 'must authenticate before announcing',
|
||||||
|
moduleAnnounceIdentityInvalid: 'module identity must include kind=plugin and a plugin id for event \'module:announce\'',
|
||||||
|
moduleAnnounceIndexInvalid: 'the field \'index\' must be a non-negative integer for event \'module:announce\'',
|
||||||
|
moduleAnnounceNameInvalid: 'the field \'name\' must be a non-empty string for event \'module:announce\'',
|
||||||
|
moduleNotFound: 'module not found, it hasn\'t announced itself or the name is incorrect',
|
||||||
|
notAuthenticated: 'not authenticated',
|
||||||
|
uiConfigureModuleIndexInvalid: 'the field \'moduleIndex\' must be a non-negative integer for event \'ui:configure\'',
|
||||||
|
uiConfigureModuleNameInvalid: 'the field \'moduleName\' can\'t be empty for event \'ui:configure\'',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ServerErrorCode
|
||||||
|
= | 'invalid-event-format'
|
||||||
|
| 'invalid-json'
|
||||||
|
| 'invalid-token'
|
||||||
|
| 'module-announce-identity-invalid'
|
||||||
|
| 'module-announce-index-invalid'
|
||||||
|
| 'module-announce-name-invalid'
|
||||||
|
| 'module-not-found'
|
||||||
|
| 'must-authenticate-before-announcing'
|
||||||
|
| 'not-authenticated'
|
||||||
|
| 'ui-configure-module-index-invalid'
|
||||||
|
| 'ui-configure-module-name-invalid'
|
||||||
|
| 'unknown'
|
||||||
|
|
||||||
|
export interface ParsedServerErrorMessage {
|
||||||
|
authentication: boolean
|
||||||
|
code: ServerErrorCode
|
||||||
|
message: string
|
||||||
|
recoverable: boolean
|
||||||
|
terminal: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createInvalidJsonServerErrorMessage(errorMessage: string) {
|
||||||
|
return `invalid JSON, error: ${errorMessage}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseServerErrorMessage(message: string): ParsedServerErrorMessage {
|
||||||
|
if (message === ServerErrorMessages.invalidToken) {
|
||||||
|
return {
|
||||||
|
authentication: true,
|
||||||
|
code: 'invalid-token',
|
||||||
|
message,
|
||||||
|
recoverable: false,
|
||||||
|
terminal: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message === ServerErrorMessages.notAuthenticated) {
|
||||||
|
return {
|
||||||
|
authentication: true,
|
||||||
|
code: 'not-authenticated',
|
||||||
|
message,
|
||||||
|
recoverable: true,
|
||||||
|
terminal: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message === ServerErrorMessages.mustAuthenticateBeforeAnnouncing) {
|
||||||
|
return {
|
||||||
|
authentication: true,
|
||||||
|
code: 'must-authenticate-before-announcing',
|
||||||
|
message,
|
||||||
|
recoverable: true,
|
||||||
|
terminal: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message === ServerErrorMessages.invalidEventFormat) {
|
||||||
|
return {
|
||||||
|
authentication: false,
|
||||||
|
code: 'invalid-event-format',
|
||||||
|
message,
|
||||||
|
recoverable: false,
|
||||||
|
terminal: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.startsWith('invalid JSON, error: ')) {
|
||||||
|
return {
|
||||||
|
authentication: false,
|
||||||
|
code: 'invalid-json',
|
||||||
|
message,
|
||||||
|
recoverable: false,
|
||||||
|
terminal: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message === ServerErrorMessages.moduleAnnounceNameInvalid) {
|
||||||
|
return {
|
||||||
|
authentication: false,
|
||||||
|
code: 'module-announce-name-invalid',
|
||||||
|
message,
|
||||||
|
recoverable: false,
|
||||||
|
terminal: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message === ServerErrorMessages.moduleAnnounceIndexInvalid) {
|
||||||
|
return {
|
||||||
|
authentication: false,
|
||||||
|
code: 'module-announce-index-invalid',
|
||||||
|
message,
|
||||||
|
recoverable: false,
|
||||||
|
terminal: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message === ServerErrorMessages.moduleAnnounceIdentityInvalid) {
|
||||||
|
return {
|
||||||
|
authentication: false,
|
||||||
|
code: 'module-announce-identity-invalid',
|
||||||
|
message,
|
||||||
|
recoverable: false,
|
||||||
|
terminal: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message === ServerErrorMessages.moduleNotFound) {
|
||||||
|
return {
|
||||||
|
authentication: false,
|
||||||
|
code: 'module-not-found',
|
||||||
|
message,
|
||||||
|
recoverable: false,
|
||||||
|
terminal: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message === ServerErrorMessages.uiConfigureModuleNameInvalid) {
|
||||||
|
return {
|
||||||
|
authentication: false,
|
||||||
|
code: 'ui-configure-module-name-invalid',
|
||||||
|
message,
|
||||||
|
recoverable: false,
|
||||||
|
terminal: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message === ServerErrorMessages.uiConfigureModuleIndexInvalid) {
|
||||||
|
return {
|
||||||
|
authentication: false,
|
||||||
|
code: 'ui-configure-module-index-invalid',
|
||||||
|
message,
|
||||||
|
recoverable: false,
|
||||||
|
terminal: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
authentication: false,
|
||||||
|
code: 'unknown',
|
||||||
|
message,
|
||||||
|
recoverable: false,
|
||||||
|
terminal: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAuthenticationServerErrorMessage(message: string) {
|
||||||
|
return parseServerErrorMessage(message).authentication
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTerminalAuthenticationServerErrorMessage(message: string) {
|
||||||
|
const parsed = parseServerErrorMessage(message)
|
||||||
|
return parsed.authentication && parsed.terminal
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
console.warn('import @proj-airi/server-shared/types instead')
|
export * from './errors'
|
||||||
|
export * from './types'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { defineConfig } from 'tsdown'
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
entry: {
|
entry: {
|
||||||
|
'index': 'src/index.ts',
|
||||||
'types/index': 'src/types/index.ts',
|
'types/index': 'src/types/index.ts',
|
||||||
},
|
},
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
|
|||||||
Generated
+8
-34
@@ -1848,7 +1848,7 @@ importers:
|
|||||||
version: 5.0.0(vue@3.5.29(typescript@5.9.3))
|
version: 5.0.0(vue@3.5.29(typescript@5.9.3))
|
||||||
'@intlify/unplugin-vue-i18n':
|
'@intlify/unplugin-vue-i18n':
|
||||||
specifier: ^11.0.7
|
specifier: ^11.0.7
|
||||||
version: 11.0.7(@vue/compiler-dom@3.5.29)(eslint@9.39.3(jiti@2.6.1))(rollup@4.59.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-i18n@11.2.8(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))
|
version: 11.0.7(@vue/compiler-dom@3.5.29)(eslint@9.39.3(jiti@2.6.1))(rollup@4.59.0)(typescript@5.9.3)(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-i18n@11.2.8(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))
|
||||||
'@mdit/plugin-footnote':
|
'@mdit/plugin-footnote':
|
||||||
specifier: ^0.22.4
|
specifier: ^0.22.4
|
||||||
version: 0.22.4(markdown-it@14.1.1)
|
version: 0.22.4(markdown-it@14.1.1)
|
||||||
@@ -1908,7 +1908,7 @@ importers:
|
|||||||
version: 0.1.3
|
version: 0.1.3
|
||||||
unplugin-yaml:
|
unplugin-yaml:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0(@nuxt/kit@3.20.2(magicast@0.5.2))(esbuild@0.27.2)(rolldown@1.0.0-rc.3)(rollup@4.59.0)(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
version: 4.0.0(esbuild@0.27.2)(rolldown@1.0.0-rc.3)(rollup@4.59.0)(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
vitepress:
|
vitepress:
|
||||||
specifier: ^2.0.0-alpha.16
|
specifier: ^2.0.0-alpha.16
|
||||||
version: 2.0.0-alpha.16(@types/node@24.10.14)(change-case@5.4.4)(fuse.js@7.1.0)(idb-keyval@6.2.2)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(nprogress@0.2.0)(postcss@8.5.6)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
|
version: 2.0.0-alpha.16(@types/node@24.10.14)(change-case@5.4.4)(fuse.js@7.1.0)(idb-keyval@6.2.2)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(nprogress@0.2.0)(postcss@8.5.6)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
|
||||||
@@ -2233,6 +2233,9 @@ importers:
|
|||||||
|
|
||||||
packages/server-sdk:
|
packages/server-sdk:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@moeru/std':
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 0.1.0-beta.17
|
||||||
'@proj-airi/server-shared':
|
'@proj-airi/server-shared':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../server-shared
|
version: link:../server-shared
|
||||||
@@ -2242,10 +2245,6 @@ importers:
|
|||||||
superjson:
|
superjson:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 2.2.6
|
version: 2.2.6
|
||||||
devDependencies:
|
|
||||||
'@moeru/std':
|
|
||||||
specifier: 'catalog:'
|
|
||||||
version: 0.1.0-beta.17
|
|
||||||
|
|
||||||
packages/server-shared:
|
packages/server-shared:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3261,7 +3260,7 @@ importers:
|
|||||||
version: 14.1.0(vue@3.5.29(typescript@5.9.3))
|
version: 14.1.0(vue@3.5.29(typescript@5.9.3))
|
||||||
'@wxt-dev/module-vue':
|
'@wxt-dev/module-vue':
|
||||||
specifier: ^1.0.3
|
specifier: ^1.0.3
|
||||||
version: 1.0.3(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
version: 1.0.3(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
nanoid:
|
nanoid:
|
||||||
specifier: ^5.1.6
|
specifier: ^5.1.6
|
||||||
version: 5.1.6
|
version: 5.1.6
|
||||||
@@ -19423,31 +19422,6 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
'@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.29)(eslint@9.39.3(jiti@2.6.1))(rollup@4.59.0)(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-i18n@11.2.8(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))':
|
|
||||||
dependencies:
|
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1))
|
|
||||||
'@intlify/bundle-utils': 11.0.7(vue-i18n@11.2.8(vue@3.5.29(typescript@5.9.3)))
|
|
||||||
'@intlify/shared': 11.2.8
|
|
||||||
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.2.8)(@vue/compiler-dom@3.5.29)(vue-i18n@11.2.8(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))
|
|
||||||
'@rollup/pluginutils': 5.3.0(rollup@4.59.0)
|
|
||||||
'@typescript-eslint/scope-manager': 8.56.1
|
|
||||||
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
|
|
||||||
debug: 4.4.3
|
|
||||||
fast-glob: 3.3.3
|
|
||||||
pathe: 2.0.3
|
|
||||||
picocolors: 1.1.1
|
|
||||||
unplugin: 2.3.11
|
|
||||||
vite: 7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
|
||||||
vue: 3.5.29(typescript@5.9.3)
|
|
||||||
optionalDependencies:
|
|
||||||
vue-i18n: 11.2.8(vue@3.5.29(typescript@5.9.3))
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@vue/compiler-dom'
|
|
||||||
- eslint
|
|
||||||
- rollup
|
|
||||||
- supports-color
|
|
||||||
- typescript
|
|
||||||
|
|
||||||
'@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.29)(eslint@9.39.3(jiti@2.6.1))(rollup@4.59.0)(typescript@5.9.3)(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-i18n@11.2.8(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))':
|
'@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.29)(eslint@9.39.3(jiti@2.6.1))(rollup@4.59.0)(typescript@5.9.3)(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-i18n@11.2.8(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1))
|
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1))
|
||||||
@@ -23682,9 +23656,9 @@ snapshots:
|
|||||||
'@types/filesystem': 0.0.36
|
'@types/filesystem': 0.0.36
|
||||||
'@types/har-format': 1.2.16
|
'@types/har-format': 1.2.16
|
||||||
|
|
||||||
'@wxt-dev/module-vue@1.0.3(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
'@wxt-dev/module-vue@1.0.3(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitejs/plugin-vue': 6.0.4(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))
|
'@vitejs/plugin-vue': 6.0.4(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))
|
||||||
wxt: 0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
wxt: 0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- vite
|
- vite
|
||||||
|
|||||||
+5
-4
@@ -5,12 +5,13 @@ export default defineConfig({
|
|||||||
projects: [
|
projects: [
|
||||||
'apps/server',
|
'apps/server',
|
||||||
'apps/stage-tamagotchi',
|
'apps/stage-tamagotchi',
|
||||||
'packages/stage-ui',
|
|
||||||
'packages/plugin-sdk',
|
|
||||||
'packages/cap-vite',
|
|
||||||
'packages/vite-plugin-warpdrive',
|
|
||||||
'packages/audio-pipelines-transcribe',
|
'packages/audio-pipelines-transcribe',
|
||||||
|
'packages/cap-vite',
|
||||||
|
'packages/plugin-sdk',
|
||||||
'packages/server-runtime',
|
'packages/server-runtime',
|
||||||
|
'packages/server-sdk',
|
||||||
|
'packages/stage-ui',
|
||||||
|
'packages/vite-plugin-warpdrive',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user