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.
Files changed (94) hide show
  1. package/AGENTS.md +11 -0
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +29 -0
  4. package/docs/guides/07-examples.mdx +56 -0
  5. package/docs/index.mdx +13 -0
  6. package/examples/README.md +15 -0
  7. package/examples/playground/AGENTS.md +11 -0
  8. package/examples/playground/DEPLOY.md +106 -0
  9. package/examples/playground/README.md +19 -0
  10. package/examples/playground/activity-feed.test.ts +10 -0
  11. package/examples/playground/app/agent/[agentId]/agent-client.tsx +376 -0
  12. package/examples/playground/app/agent/[agentId]/page.tsx +29 -0
  13. package/examples/playground/app/agent/events/route.ts +4 -0
  14. package/examples/playground/app/agent/model.ts +3 -0
  15. package/examples/playground/app/agent/new-agent-session.tsx +98 -0
  16. package/examples/playground/app/agent/page.tsx +25 -0
  17. package/examples/playground/app/agent/scheduler/route.ts +5 -0
  18. package/examples/playground/app/agent/server.ts +153 -0
  19. package/examples/playground/app/agent/session.ts +13 -0
  20. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +682 -0
  21. package/examples/playground/app/canvas/[canvasId]/canvas-replay.test.ts +68 -0
  22. package/examples/playground/app/canvas/[canvasId]/canvas-replay.ts +19 -0
  23. package/examples/playground/app/canvas/[canvasId]/page.tsx +22 -0
  24. package/examples/playground/app/canvas/[canvasId]/session.ts +19 -0
  25. package/examples/playground/app/canvas/events/route.ts +13 -0
  26. package/examples/playground/app/canvas/model.ts +94 -0
  27. package/examples/playground/app/canvas/open-canvas.tsx +40 -0
  28. package/examples/playground/app/canvas/page.tsx +20 -0
  29. package/examples/playground/app/canvas/server.ts +9 -0
  30. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +118 -0
  31. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +316 -0
  32. package/examples/playground/app/chat/[chatId]/chat-client.tsx +922 -0
  33. package/examples/playground/app/chat/[chatId]/chat-view.test.ts +152 -0
  34. package/examples/playground/app/chat/[chatId]/chat-view.ts +101 -0
  35. package/examples/playground/app/chat/[chatId]/composer.test.ts +44 -0
  36. package/examples/playground/app/chat/[chatId]/composer.ts +30 -0
  37. package/examples/playground/app/chat/[chatId]/page.tsx +30 -0
  38. package/examples/playground/app/chat/[chatId]/session.ts +7 -0
  39. package/examples/playground/app/chat/events/route.ts +7 -0
  40. package/examples/playground/app/chat/model.test.ts +155 -0
  41. package/examples/playground/app/chat/model.ts +310 -0
  42. package/examples/playground/app/chat/new-conversation.tsx +16 -0
  43. package/examples/playground/app/chat/page.tsx +25 -0
  44. package/examples/playground/app/chat/scheduler/route.ts +5 -0
  45. package/examples/playground/app/chat/server.ts +184 -0
  46. package/examples/playground/app/components/activity-feed.tsx +54 -0
  47. package/examples/playground/app/components/connection-pill.tsx +29 -0
  48. package/examples/playground/app/counter/counter-client.tsx +72 -0
  49. package/examples/playground/app/counter/events/route.ts +4 -0
  50. package/examples/playground/app/counter/model.test.ts +36 -0
  51. package/examples/playground/app/counter/model.ts +31 -0
  52. package/examples/playground/app/counter/page.tsx +24 -0
  53. package/examples/playground/app/counter/server.ts +9 -0
  54. package/examples/playground/app/counter/session.ts +13 -0
  55. package/examples/playground/app/documents/[documentId]/code-editor.tsx +80 -0
  56. package/examples/playground/app/documents/[documentId]/document-client.tsx +525 -0
  57. package/examples/playground/app/documents/[documentId]/page.tsx +23 -0
  58. package/examples/playground/app/documents/[documentId]/session.ts +7 -0
  59. package/examples/playground/app/documents/events/route.ts +4 -0
  60. package/examples/playground/app/documents/model.ts +55 -0
  61. package/examples/playground/app/documents/open-document.tsx +40 -0
  62. package/examples/playground/app/documents/page.tsx +22 -0
  63. package/examples/playground/app/documents/server.ts +9 -0
  64. package/examples/playground/app/globals.css +2078 -0
  65. package/examples/playground/app/layout.tsx +44 -0
  66. package/examples/playground/app/orders/[orderId]/order-client.tsx +140 -0
  67. package/examples/playground/app/orders/[orderId]/page.tsx +29 -0
  68. package/examples/playground/app/orders/[orderId]/session.ts +11 -0
  69. package/examples/playground/app/orders/create/route.ts +30 -0
  70. package/examples/playground/app/orders/events/route.ts +4 -0
  71. package/examples/playground/app/orders/model.ts +79 -0
  72. package/examples/playground/app/orders/new-order-form.tsx +98 -0
  73. package/examples/playground/app/orders/page.tsx +22 -0
  74. package/examples/playground/app/orders/scheduler/route.ts +5 -0
  75. package/examples/playground/app/orders/server.ts +50 -0
  76. package/examples/playground/app/page.tsx +102 -0
  77. package/examples/playground/app/recovery/[recoveryId]/page.tsx +31 -0
  78. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +144 -0
  79. package/examples/playground/app/recovery/[recoveryId]/session.ts +7 -0
  80. package/examples/playground/app/recovery/events/route.ts +3 -0
  81. package/examples/playground/app/recovery/model.ts +55 -0
  82. package/examples/playground/app/recovery/new-recovery-session.tsx +20 -0
  83. package/examples/playground/app/recovery/page.tsx +22 -0
  84. package/examples/playground/app/recovery/scheduler/route.ts +7 -0
  85. package/examples/playground/app/recovery/server.ts +53 -0
  86. package/examples/playground/app/recovery/start/route.ts +41 -0
  87. package/examples/playground/css.d.ts +4 -0
  88. package/examples/playground/lib/store.ts +15 -0
  89. package/examples/playground/next-env.d.ts +5 -0
  90. package/examples/playground/next.config.ts +10 -0
  91. package/examples/playground/package.json +46 -0
  92. package/examples/playground/tsconfig.json +37 -0
  93. package/examples/playground/vercel.json +40 -0
  94. package/package.json +5 -2
@@ -0,0 +1,4 @@
1
+ import { counterServer } from '../server'
2
+
3
+ export const GET = counterServer.fetch
4
+ export const POST = counterServer.fetch
@@ -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,9 @@
1
+ import 'server-only'
2
+ import { createServer } from 'experimental-a2/server'
3
+ import { store } from '@/lib/store'
4
+ import { counter } from './model'
5
+
6
+ export const counterServer = createServer({
7
+ ...(store ? { store } : {}),
8
+ contract: counter,
9
+ })
@@ -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
+ }