deepspace 0.5.0 → 0.5.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.
@@ -175,7 +175,11 @@ export function useDocEditor({
175
175
  // in dev (the editor view is destroyed and re-created, and any subscriber
176
176
  // that runs in between throws on `editor.view`). Defer to a useEffect.
177
177
  immediatelyRender: false,
178
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
178
+ // Bare directive on purpose: naming @typescript-eslint/no-explicit-any
179
+ // here is a hard "unknown rule" error under the scaffolded app's
180
+ // minimal eslint config (which never registers that plugin), while the
181
+ // SDK monorepo's config does enable the rule and needs the suppression.
182
+ // eslint-disable-next-line
179
183
  extensions: extensions as any,
180
184
  editorProps: {
181
185
  transformPastedHTML: stripCollaborationArtifactsFromHTML,
@@ -37,8 +37,13 @@ export default function ChatPage({
37
37
  }, [channelId, isMember, markAsRead])
38
38
 
39
39
  if (status !== 'ready' || !channelId) {
40
+ // data-testid lets tests (and humans reading a trace) distinguish "stuck
41
+ // waiting for the default channel" from "route never mounted".
40
42
  return (
41
- <div className={`flex items-center justify-center h-full ${className ?? ''}`}>
43
+ <div
44
+ data-testid="chat-page-loading"
45
+ className={`flex items-center justify-center h-full ${className ?? ''}`}
46
+ >
42
47
  <div className="text-muted-foreground text-sm">Loading chat...</div>
43
48
  </div>
44
49
  )
@@ -26,12 +26,24 @@ export function useChatChannel(channelName: string = DEFAULT_CHANNEL_NAME) {
26
26
  const { channels, status, create } = useChannels()
27
27
  const hasInitialized = useRef(false)
28
28
 
29
- const defaultChannel = channels.find(
30
- (c: RecordData<Channel>) =>
31
- c.data.name === channelName &&
32
- c.data.type === 'public' &&
33
- !c.data.archived
34
- )
29
+ // Nothing enforces name uniqueness server-side, so two first visitors can
30
+ // race the one-shot init below and BOTH create the default channel. With a
31
+ // bare .find(), each client then resolves whichever duplicate its local
32
+ // snapshot orders first — split-brain: A and B post into different rooms
33
+ // and never see each other. Pick deterministically (oldest createdAt,
34
+ // recordId tie-break) so every client converges on the same channel once
35
+ // the duplicates sync.
36
+ const defaultChannel = channels
37
+ .filter(
38
+ (c: RecordData<Channel>) =>
39
+ c.data.name === channelName &&
40
+ c.data.type === 'public' &&
41
+ !c.data.archived,
42
+ )
43
+ .sort(
44
+ (a: RecordData<Channel>, b: RecordData<Channel>) =>
45
+ a.createdAt.localeCompare(b.createdAt) || a.recordId.localeCompare(b.recordId),
46
+ )[0]
35
47
 
36
48
  const { isMember, join, status: membersStatus } = useChannelMembers(defaultChannel?.recordId)
37
49
 
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "deepspace",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
+ "engines": {
5
+ "node": ">=22.15.0"
6
+ },
4
7
  "description": "DeepSpace SDK — build real-time collaborative apps on Cloudflare Workers",
5
8
  "type": "module",
6
9
  "sideEffects": false,
@@ -41,14 +44,15 @@
41
44
  },
42
45
  "dependencies": {
43
46
  "@clack/prompts": "^1.2.0",
44
- "better-auth": "1.5.6",
47
+ "better-auth": "1.6.23",
45
48
  "citty": "^0.2.0",
46
49
  "cross-spawn": "^7.0.6",
47
50
  "esbuild": "^0.25.0",
48
51
  "hono": "^4.6.0",
49
52
  "jose": "^6.1.0",
50
53
  "smol-toml": "^1.6.1",
51
- "yjs": "^13.6.0"
54
+ "yjs": "^13.6.0",
55
+ "zod": "^4.3.6"
52
56
  },
53
57
  "peerDependencies": {
54
58
  "@ai-sdk/anthropic": "^2.0.0",
@@ -97,16 +97,18 @@ function copyFile(src, dest) {
97
97
  }
98
98
 
99
99
  // Copy + rewrite leading `../` imports. Used when a page file is redirected
100
- // into src/pages/(protected)/, which adds one directory level — every relative
101
- // import that traverses upward needs one extra `../`. Matches static
102
- // `from '...'`, side-effect `import '...'`, and dynamic/type `import('...')`
103
- // (e.g. `import('../hooks/x').T` in a type position, or `await import('../x')`).
100
+ // into a deeper route group (e.g. src/pages/(app)/ adds one directory level,
101
+ // src/pages/(app)/(protected)/ adds two) — every relative import that
102
+ // traverses upward needs `levels` extra `../`. Matches static `from '...'`,
103
+ // side-effect `import '...'`, and dynamic/type `import('...')` (e.g.
104
+ // `import('../hooks/x').T` in a type position, or `await import('../x')`).
104
105
  // Same-directory (`./...`) and bare-package imports are untouched.
105
- function copyFileWithImportShift(src, dest) {
106
+ function copyFileWithImportShift(src, dest, levels) {
106
107
  const destDir = path.dirname(dest)
107
108
  if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true })
108
109
  const content = fs.readFileSync(src, 'utf-8')
109
- const shifted = content.replace(/(from\s+|import\s+|import\()(['"])\.\.\//g, '$1$2../../')
110
+ const prefix = '../'.repeat(levels)
111
+ const shifted = content.replace(/(from\s+|import\s+|import\()(['"])(\.\.\/)/g, `$1$2${prefix}$3`)
110
112
  fs.writeFileSync(dest, shifted)
111
113
  }
112
114
 
@@ -114,18 +116,21 @@ function copyFileWithImportShift(src, dest) {
114
116
  // Install files
115
117
  // ---------------------------------------------------------------------------
116
118
 
117
- // When a feature's route is protected, page files must land under
118
- // src/pages/(protected)/ so generouted's route group wraps them in <AuthGate>.
119
- // `(protected)` is a folder-name-only group — the URL path is unchanged.
120
- // Only page files (src/pages/*) get rewritten; schemas, components, etc.
121
- // keep their declared destinations.
119
+ // Page files must land under src/pages/(app)/ so the (app)/_layout.tsx route
120
+ // group wraps them in the DeepSpace providers (auth + realtime records) — the
121
+ // top level of src/pages/ is reserved for static pages that mount no
122
+ // providers. Protected pages go one level deeper, under (app)/(protected)/,
123
+ // so the nested layout also wraps them in <AuthGate>. Both are
124
+ // folder-name-only route groups — the URL path is unchanged. Only page files
125
+ // (src/pages/*) get redirected; schemas, components, etc. keep their declared
126
+ // destinations. Page files already placed in the (app) group are left as-is.
122
127
  function resolveDestPath(fileDest, route) {
123
128
  const PAGES = 'src/pages/'
124
- const PROTECTED = 'src/pages/(protected)/'
129
+ const APP = 'src/pages/(app)/'
130
+ if (!fileDest.startsWith(PAGES) || fileDest.startsWith(APP)) return fileDest
131
+ const rest = fileDest.slice(PAGES.length)
125
132
  const isProtected = route && route.protected !== false
126
- if (!isProtected) return fileDest
127
- if (!fileDest.startsWith(PAGES) || fileDest.startsWith(PROTECTED)) return fileDest
128
- return PROTECTED + fileDest.slice(PAGES.length)
133
+ return isProtected ? `${APP}(protected)/${rest}` : `${APP}${rest}`
129
134
  }
130
135
 
131
136
  function installFeature(config, targetDir) {
@@ -137,6 +142,10 @@ function installFeature(config, targetDir) {
137
142
  const resolvedDest = resolveDestPath(file.dest, config.route)
138
143
  const destPath = path.join(targetDir, resolvedDest)
139
144
  const wasRedirected = resolvedDest !== file.dest
145
+ // Each extra path segment in the resolved dest is one directory level the
146
+ // page dropped into (e.g. (app)/ = 1, (app)/(protected)/ = 2), which is
147
+ // exactly how many `../` its upward relative imports must gain.
148
+ const shiftLevels = resolvedDest.split('/').length - file.dest.split('/').length
140
149
 
141
150
  if (!fs.existsSync(srcPath)) {
142
151
  console.error(` Warning: source not found: ${file.src}`)
@@ -149,7 +158,7 @@ function installFeature(config, targetDir) {
149
158
  } else {
150
159
  const replacing = fs.existsSync(destPath)
151
160
  if (wasRedirected) {
152
- copyFileWithImportShift(srcPath, destPath)
161
+ copyFileWithImportShift(srcPath, destPath, shiftLevels)
153
162
  } else {
154
163
  copyFile(srcPath, destPath)
155
164
  }