experimental-a2 0.8.0 → 0.8.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/AGENTS.md +11 -0
- package/CHANGELOG.md +8 -0
- package/README.md +29 -0
- package/docs/guides/07-examples.mdx +56 -0
- package/docs/index.mdx +13 -0
- package/examples/README.md +15 -0
- package/examples/playground/AGENTS.md +11 -0
- package/examples/playground/DEPLOY.md +106 -0
- package/examples/playground/README.md +19 -0
- package/examples/playground/activity-feed.test.ts +10 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +376 -0
- package/examples/playground/app/agent/[agentId]/page.tsx +29 -0
- package/examples/playground/app/agent/events/route.ts +4 -0
- package/examples/playground/app/agent/model.ts +3 -0
- package/examples/playground/app/agent/new-agent-session.tsx +98 -0
- package/examples/playground/app/agent/page.tsx +25 -0
- package/examples/playground/app/agent/scheduler/route.ts +5 -0
- package/examples/playground/app/agent/server.ts +153 -0
- package/examples/playground/app/agent/session.ts +13 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +682 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-replay.test.ts +68 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-replay.ts +19 -0
- package/examples/playground/app/canvas/[canvasId]/page.tsx +22 -0
- package/examples/playground/app/canvas/[canvasId]/session.ts +19 -0
- package/examples/playground/app/canvas/events/route.ts +13 -0
- package/examples/playground/app/canvas/model.ts +94 -0
- package/examples/playground/app/canvas/open-canvas.tsx +40 -0
- package/examples/playground/app/canvas/page.tsx +20 -0
- package/examples/playground/app/canvas/server.ts +9 -0
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +118 -0
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +316 -0
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +922 -0
- package/examples/playground/app/chat/[chatId]/chat-view.test.ts +152 -0
- package/examples/playground/app/chat/[chatId]/chat-view.ts +101 -0
- package/examples/playground/app/chat/[chatId]/composer.test.ts +44 -0
- package/examples/playground/app/chat/[chatId]/composer.ts +30 -0
- package/examples/playground/app/chat/[chatId]/page.tsx +30 -0
- package/examples/playground/app/chat/[chatId]/session.ts +7 -0
- package/examples/playground/app/chat/events/route.ts +7 -0
- package/examples/playground/app/chat/model.test.ts +155 -0
- package/examples/playground/app/chat/model.ts +310 -0
- package/examples/playground/app/chat/new-conversation.tsx +16 -0
- package/examples/playground/app/chat/page.tsx +25 -0
- package/examples/playground/app/chat/scheduler/route.ts +5 -0
- package/examples/playground/app/chat/server.ts +184 -0
- package/examples/playground/app/components/activity-feed.tsx +54 -0
- package/examples/playground/app/components/connection-pill.tsx +29 -0
- package/examples/playground/app/counter/counter-client.tsx +72 -0
- package/examples/playground/app/counter/events/route.ts +4 -0
- package/examples/playground/app/counter/model.test.ts +36 -0
- package/examples/playground/app/counter/model.ts +31 -0
- package/examples/playground/app/counter/page.tsx +24 -0
- package/examples/playground/app/counter/server.ts +9 -0
- package/examples/playground/app/counter/session.ts +13 -0
- package/examples/playground/app/documents/[documentId]/code-editor.tsx +80 -0
- package/examples/playground/app/documents/[documentId]/document-client.tsx +525 -0
- package/examples/playground/app/documents/[documentId]/page.tsx +23 -0
- package/examples/playground/app/documents/[documentId]/session.ts +7 -0
- package/examples/playground/app/documents/events/route.ts +4 -0
- package/examples/playground/app/documents/model.ts +55 -0
- package/examples/playground/app/documents/open-document.tsx +40 -0
- package/examples/playground/app/documents/page.tsx +22 -0
- package/examples/playground/app/documents/server.ts +9 -0
- package/examples/playground/app/globals.css +2078 -0
- package/examples/playground/app/layout.tsx +44 -0
- package/examples/playground/app/orders/[orderId]/order-client.tsx +140 -0
- package/examples/playground/app/orders/[orderId]/page.tsx +29 -0
- package/examples/playground/app/orders/[orderId]/session.ts +11 -0
- package/examples/playground/app/orders/create/route.ts +30 -0
- package/examples/playground/app/orders/events/route.ts +4 -0
- package/examples/playground/app/orders/model.ts +79 -0
- package/examples/playground/app/orders/new-order-form.tsx +98 -0
- package/examples/playground/app/orders/page.tsx +22 -0
- package/examples/playground/app/orders/scheduler/route.ts +5 -0
- package/examples/playground/app/orders/server.ts +50 -0
- package/examples/playground/app/page.tsx +102 -0
- package/examples/playground/app/recovery/[recoveryId]/page.tsx +31 -0
- package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +144 -0
- package/examples/playground/app/recovery/[recoveryId]/session.ts +7 -0
- package/examples/playground/app/recovery/events/route.ts +3 -0
- package/examples/playground/app/recovery/model.ts +55 -0
- package/examples/playground/app/recovery/new-recovery-session.tsx +20 -0
- package/examples/playground/app/recovery/page.tsx +22 -0
- package/examples/playground/app/recovery/scheduler/route.ts +7 -0
- package/examples/playground/app/recovery/server.ts +53 -0
- package/examples/playground/app/recovery/start/route.ts +41 -0
- package/examples/playground/css.d.ts +4 -0
- package/examples/playground/lib/store.ts +15 -0
- package/examples/playground/next-env.d.ts +5 -0
- package/examples/playground/next.config.ts +10 -0
- package/examples/playground/package.json +46 -0
- package/examples/playground/tsconfig.json +37 -0
- package/examples/playground/vercel.json +40 -0
- package/package.json +5 -2
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ContractEvent } from 'experimental-a2'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { counter, counterReducer } from './model'
|
|
4
|
+
|
|
5
|
+
type CounterEvents = typeof counter.events
|
|
6
|
+
type CounterEvent<
|
|
7
|
+
K extends keyof CounterEvents & string = keyof CounterEvents & string,
|
|
8
|
+
> = ContractEvent<CounterEvents, K>
|
|
9
|
+
|
|
10
|
+
const event = <K extends keyof CounterEvents & string>(
|
|
11
|
+
index: number,
|
|
12
|
+
type: K,
|
|
13
|
+
): CounterEvent<K> =>
|
|
14
|
+
({
|
|
15
|
+
id: `event-${index}`,
|
|
16
|
+
type,
|
|
17
|
+
payload: {},
|
|
18
|
+
index,
|
|
19
|
+
sessionId: 'shared',
|
|
20
|
+
createdAt: new Date(index * 1_000),
|
|
21
|
+
}) as CounterEvent<K>
|
|
22
|
+
|
|
23
|
+
describe('counter reducer', () => {
|
|
24
|
+
it('folds increments and decrements in log order', () => {
|
|
25
|
+
const state = [
|
|
26
|
+
event(1, 'incremented'),
|
|
27
|
+
event(2, 'incremented'),
|
|
28
|
+
event(3, 'decremented'),
|
|
29
|
+
].reduce(
|
|
30
|
+
(current, nextEvent) => counterReducer.fold(current, nextEvent),
|
|
31
|
+
counterReducer.initialState,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
expect(state).toEqual({ value: 1 })
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as a2 from 'experimental-a2'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
export const counter = a2.contract({
|
|
5
|
+
name: 'counter',
|
|
6
|
+
events: {
|
|
7
|
+
incremented: z.object({}),
|
|
8
|
+
decremented: z.object({}),
|
|
9
|
+
},
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const counterState = z.object({ value: z.number().int() })
|
|
13
|
+
|
|
14
|
+
export type CounterState = z.output<typeof counterState>
|
|
15
|
+
|
|
16
|
+
export const counterReducer = counter
|
|
17
|
+
.reducer({
|
|
18
|
+
name: 'counter-value',
|
|
19
|
+
initialState: { value: 0 },
|
|
20
|
+
stateSchema: counterState,
|
|
21
|
+
})
|
|
22
|
+
.fold((state, event) => {
|
|
23
|
+
switch (event.type) {
|
|
24
|
+
case 'incremented':
|
|
25
|
+
return { value: state.value + 1 }
|
|
26
|
+
case 'decremented':
|
|
27
|
+
return { value: state.value - 1 }
|
|
28
|
+
default:
|
|
29
|
+
return state
|
|
30
|
+
}
|
|
31
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { counterReducer } from './model'
|
|
3
|
+
import { counterServer } from './server'
|
|
4
|
+
import { CounterClient } from './counter-client'
|
|
5
|
+
import { SessionProvider } from './session'
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic'
|
|
8
|
+
|
|
9
|
+
const COUNTER_SESSION_ID = 'shared'
|
|
10
|
+
|
|
11
|
+
export default async function CounterPage(): Promise<ReactNode> {
|
|
12
|
+
const session = counterServer.session(COUNTER_SESSION_ID)
|
|
13
|
+
const { state, index } = await session.state(counterReducer)
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<SessionProvider
|
|
17
|
+
sessionId={COUNTER_SESSION_ID}
|
|
18
|
+
initialState={state}
|
|
19
|
+
initialIndex={index}
|
|
20
|
+
>
|
|
21
|
+
<CounterClient />
|
|
22
|
+
</SessionProvider>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { createClient } from 'experimental-a2/client'
|
|
3
|
+
import { createReact } from 'experimental-a2/react'
|
|
4
|
+
import { counterReducer } from './model'
|
|
5
|
+
|
|
6
|
+
export const counterClient = createClient({
|
|
7
|
+
reducer: counterReducer,
|
|
8
|
+
api: '/counter/events',
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export const { SessionProvider, useSession } = createReact({
|
|
12
|
+
client: counterClient,
|
|
13
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { useEffect, useRef, useState } from 'react'
|
|
4
|
+
import { basicSetup } from 'codemirror'
|
|
5
|
+
import { Compartment, EditorState } from '@codemirror/state'
|
|
6
|
+
import { EditorView, placeholder } from '@codemirror/view'
|
|
7
|
+
import { yCollab } from 'y-codemirror.next'
|
|
8
|
+
import type { Awareness } from 'y-protocols/awareness'
|
|
9
|
+
import type * as Y from 'yjs'
|
|
10
|
+
|
|
11
|
+
const bodyPlaceholder =
|
|
12
|
+
'Start writing. Open this URL in another tab to collaborate…'
|
|
13
|
+
|
|
14
|
+
export function CodeMirrorEditor({
|
|
15
|
+
doc,
|
|
16
|
+
awareness,
|
|
17
|
+
disabled,
|
|
18
|
+
}: {
|
|
19
|
+
doc: Y.Doc
|
|
20
|
+
awareness: Awareness
|
|
21
|
+
disabled: boolean
|
|
22
|
+
}): ReactNode {
|
|
23
|
+
const host = useRef<HTMLDivElement>(null)
|
|
24
|
+
const view = useRef<EditorView | null>(null)
|
|
25
|
+
const [editable] = useState(() => new Compartment())
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!host.current) return
|
|
29
|
+
|
|
30
|
+
const body = doc.getText('body')
|
|
31
|
+
const editor = new EditorView({
|
|
32
|
+
parent: host.current,
|
|
33
|
+
state: EditorState.create({
|
|
34
|
+
doc: body.toString(),
|
|
35
|
+
extensions: [
|
|
36
|
+
basicSetup,
|
|
37
|
+
EditorView.lineWrapping,
|
|
38
|
+
EditorView.contentAttributes.of({
|
|
39
|
+
'aria-label': 'Document body',
|
|
40
|
+
spellcheck: 'true',
|
|
41
|
+
}),
|
|
42
|
+
placeholder(bodyPlaceholder),
|
|
43
|
+
// Awareness rides the session's presence plane: cursors and
|
|
44
|
+
// selections replicate last-writer-wins and expire, while only
|
|
45
|
+
// document changes enter the durable log.
|
|
46
|
+
yCollab(body, awareness),
|
|
47
|
+
// Upstream keeps the caret when the editor blurs (its clear
|
|
48
|
+
// branch requires focus, so it never runs); an unfocused
|
|
49
|
+
// editor should show no phantom caret.
|
|
50
|
+
EditorView.updateListener.of((update) => {
|
|
51
|
+
if (update.focusChanged && !update.view.hasFocus) {
|
|
52
|
+
awareness.setLocalStateField('cursor', null)
|
|
53
|
+
}
|
|
54
|
+
}),
|
|
55
|
+
editable.of([
|
|
56
|
+
EditorState.readOnly.of(disabled),
|
|
57
|
+
EditorView.editable.of(!disabled),
|
|
58
|
+
]),
|
|
59
|
+
],
|
|
60
|
+
}),
|
|
61
|
+
})
|
|
62
|
+
view.current = editor
|
|
63
|
+
|
|
64
|
+
return () => {
|
|
65
|
+
editor.destroy()
|
|
66
|
+
view.current = null
|
|
67
|
+
}
|
|
68
|
+
}, [doc, awareness, editable])
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
view.current?.dispatch({
|
|
72
|
+
effects: editable.reconfigure([
|
|
73
|
+
EditorState.readOnly.of(disabled),
|
|
74
|
+
EditorView.editable.of(!disabled),
|
|
75
|
+
]),
|
|
76
|
+
})
|
|
77
|
+
}, [disabled, editable])
|
|
78
|
+
|
|
79
|
+
return <div className="document-body" ref={host} />
|
|
80
|
+
}
|