Merge remote-tracking branch 'origin/master' into feat/web-message-feedback-ui
Adapt to two contract changes master introduced: - The generated Remote face now wraps every business result in RemoteResult, folding carrier failures into an ok:false branch instead of rejecting. The controller reads that envelope at its three call sites and maps a carrier failure onto the same settled shape the controls already render; three specs cover the new branch. - Client packages split their tsconfig into host and client halves, and the host aggregate now compiles any test not named *.client.spec.*. Rename this package's specs to the client convention and drop the ../connection project reference, which pointed at a solution file that no longer carries the client sources. Keep master's mount loop with its rollback-on-failure in api-remotes and add messageFeedbackRemote to it.
This commit is contained in:
@@ -36,18 +36,20 @@ const seeded: MessageFeedbackItem = {
|
||||
async function bench() {
|
||||
const ctx = new Context()
|
||||
const calls: { method: string; request: unknown }[] = []
|
||||
// The generated face wraps every business result in the carrier envelope.
|
||||
const carried = <T,>(value: T) => Promise.resolve({ ok: true as const, value })
|
||||
const messageFeedback = {
|
||||
list: (request: unknown) => {
|
||||
calls.push({ method: 'list', request })
|
||||
return Promise.resolve({ ok: true as const, value: { items: [seeded] } })
|
||||
return carried({ ok: true as const, value: { items: [seeded] } })
|
||||
},
|
||||
put: (request: unknown) => {
|
||||
calls.push({ method: 'put', request })
|
||||
return Promise.resolve({ ok: true as const, value: seeded })
|
||||
return carried({ ok: true as const, value: seeded })
|
||||
},
|
||||
delete: (request: unknown) => {
|
||||
calls.push({ method: 'delete', request })
|
||||
return Promise.resolve({ ok: true as const, value: { absent: true as const } })
|
||||
return carried({ ok: true as const, value: { absent: true as const } })
|
||||
},
|
||||
}
|
||||
class RemoteService extends Service {
|
||||
@@ -30,24 +30,34 @@ function item(overrides: Partial<MessageFeedbackItem> = {}): MessageFeedbackItem
|
||||
}
|
||||
|
||||
/** A recording fake Remote whose per-method answers are scripted per call. */
|
||||
function fakeRemote(script: Partial<MessageFeedbackRemote> = {}) {
|
||||
type Script = {
|
||||
list?: (request: unknown) => Promise<unknown>
|
||||
put?: (request: unknown) => Promise<unknown>
|
||||
delete?: (request: unknown) => Promise<unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* A recording fake Remote. Scripts return the *business* result; this wraps it
|
||||
* in the carrier envelope the generated face uses, so specs stay readable. A
|
||||
* script may also return an already-enveloped `{ok:false,error:{code,message,
|
||||
* details}}` to exercise a carrier failure.
|
||||
*/
|
||||
function fakeRemote(script: Script = {}) {
|
||||
const calls: { method: string; request: unknown }[] = []
|
||||
const record = <K extends keyof MessageFeedbackRemote>(
|
||||
method: K,
|
||||
real: MessageFeedbackRemote[K] | undefined,
|
||||
fallback: Awaited<ReturnType<MessageFeedbackRemote[K]>>,
|
||||
): MessageFeedbackRemote[K] =>
|
||||
((request: Parameters<MessageFeedbackRemote[K]>[0]) => {
|
||||
const isCarrier = (v: unknown): boolean =>
|
||||
typeof v === 'object' && v !== null && 'ok' in v && v.ok === false
|
||||
&& 'error' in v && 'details' in ((v as { error: object }).error ?? {})
|
||||
const record = (method: 'list' | 'put' | 'delete', real: Script[keyof Script], fallback: unknown) =>
|
||||
(request: never): Promise<never> => {
|
||||
calls.push({ method, request })
|
||||
return real === undefined
|
||||
? Promise.resolve(fallback)
|
||||
: (real as (input: typeof request) => ReturnType<MessageFeedbackRemote[K]>)(request)
|
||||
}) as MessageFeedbackRemote[K]
|
||||
const remote: MessageFeedbackRemote = {
|
||||
const business = real === undefined ? Promise.resolve(fallback) : real(request)
|
||||
return business.then(v => (isCarrier(v) ? v : { ok: true, value: v })) as Promise<never>
|
||||
}
|
||||
const remote = {
|
||||
list: record('list', script.list, { ok: true, value: { items: [] } }),
|
||||
put: record('put', script.put, { ok: true, value: item() }),
|
||||
delete: record('delete', script.delete, { ok: true, value: { absent: true } }),
|
||||
}
|
||||
} as unknown as MessageFeedbackRemote
|
||||
return { remote, calls }
|
||||
}
|
||||
|
||||
@@ -323,7 +333,7 @@ describe('FeedbackController', () => {
|
||||
it('swallows a rejected list that settles after disposal', async () => {
|
||||
let reject = (): void => {}
|
||||
const gate = new Promise<void>((_resolve, rejectFn) => { reject = () => { rejectFn(new Error('late')) } })
|
||||
const { remote } = fakeRemote({ list: () => gate as Promise<never> })
|
||||
const { remote } = fakeRemote({ list: () => gate })
|
||||
const controller = new FeedbackController(remote, SESSION)
|
||||
const pending = controller.ensure()
|
||||
|
||||
@@ -612,4 +622,53 @@ describe('FeedbackController', () => {
|
||||
expect(await pending).toMatchObject({ ok: false, error: { code: 'disposed' } })
|
||||
expect(calls.filter(c => c.method === 'put')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('renders a carrier failure from the Remote envelope', async () => {
|
||||
// The generated face folds transport faults into ok:false with a
|
||||
// RemoteFailure, so the controller reads them as values, not rejections.
|
||||
const { remote } = fakeRemote({
|
||||
list: () => Promise.resolve({
|
||||
ok: false,
|
||||
error: { code: 'carrier-closed', message: 'socket closed', details: {} },
|
||||
}),
|
||||
})
|
||||
const controller = new FeedbackController(remote, SESSION)
|
||||
|
||||
expect(await controller.ensure()).toEqual({
|
||||
ok: false,
|
||||
error: { code: 'carrier-closed', message: 'socket closed' },
|
||||
})
|
||||
expect(controller.getSnapshot()).toMatchObject({ status: 'error', error: 'socket closed' })
|
||||
})
|
||||
|
||||
it('renders a carrier failure on a mutation without touching the view', async () => {
|
||||
const { remote } = fakeRemote({
|
||||
put: () => Promise.resolve({
|
||||
ok: false,
|
||||
error: { code: 'carrier-closed', message: 'socket closed', details: {} },
|
||||
}),
|
||||
})
|
||||
const controller = new FeedbackController(remote, SESSION)
|
||||
|
||||
expect(await controller.rate(MSG, 'positive')).toEqual({
|
||||
ok: false,
|
||||
error: { code: 'carrier-closed', message: 'socket closed' },
|
||||
})
|
||||
expect(controller.getSnapshot().items.has(MSG)).toBe(false)
|
||||
})
|
||||
|
||||
it('renders a carrier failure on a delete', async () => {
|
||||
const { remote } = fakeRemote({
|
||||
list: () => Promise.resolve({ ok: true, value: { items: [item()] } }),
|
||||
delete: () => Promise.resolve({
|
||||
ok: false,
|
||||
error: { code: 'carrier-closed', message: 'socket closed', details: {} },
|
||||
}),
|
||||
})
|
||||
const controller = new FeedbackController(remote, SESSION)
|
||||
await controller.ensure()
|
||||
|
||||
expect(await controller.clear(MSG)).toMatchObject({ ok: false, error: { code: 'carrier-closed' } })
|
||||
expect(controller.getSnapshot().items.has(MSG)).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user