@svgrid/grid 2.1.0 → 2.1.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 (64) hide show
  1. package/dist/FlexRender.svelte +96 -96
  2. package/dist/GridFooter.svelte +178 -178
  3. package/dist/SvGrid.css +2377 -2377
  4. package/dist/SvGrid.svelte +2764 -2764
  5. package/dist/SvGridBoard.svelte +2281 -2281
  6. package/dist/SvGridDropdown.svelte +666 -666
  7. package/dist/cdn/svgrid.js +2 -2
  8. package/dist/cdn/svgrid.svelte-external.js +2 -2
  9. package/package.json +92 -92
  10. package/src/FlexRender.svelte +96 -96
  11. package/src/GridFooter.svelte +178 -178
  12. package/src/SvGrid.controller.svelte.ts +2553 -2553
  13. package/src/SvGrid.css +2377 -2377
  14. package/src/SvGrid.svelte +2764 -2764
  15. package/src/SvGrid.types.ts +944 -944
  16. package/src/SvGridBoard.svelte +2281 -2281
  17. package/src/SvGridDropdown.svelte +666 -666
  18. package/src/a11y.contract.test.ts +49 -49
  19. package/src/a11y.test.ts +59 -59
  20. package/src/a11y.ts +61 -61
  21. package/src/build-api.ts +798 -798
  22. package/src/cell-formatting.ts +169 -169
  23. package/src/cell-render.ts +469 -469
  24. package/src/collaboration.test.ts +104 -104
  25. package/src/collaboration.ts +167 -167
  26. package/src/core.performance.test.ts +30 -30
  27. package/src/core.ts +1111 -1111
  28. package/src/createGrid.svelte.ts +42 -42
  29. package/src/createGrid.test.ts +10 -10
  30. package/src/createGridState.svelte.ts +17 -17
  31. package/src/editing.test.ts +859 -859
  32. package/src/editing.ts +675 -675
  33. package/src/export-data-api.test.ts +126 -126
  34. package/src/export-format.test.ts +107 -107
  35. package/src/export-format.ts +598 -598
  36. package/src/flex-render.ts +3 -3
  37. package/src/index.ts +463 -463
  38. package/src/keyboard.test.ts +59 -59
  39. package/src/keyboard.ts +97 -97
  40. package/src/menus.ts +582 -582
  41. package/src/merge-objects.ts +48 -48
  42. package/src/render-component.ts +28 -28
  43. package/src/selection.test.ts +754 -754
  44. package/src/selection.ts +600 -600
  45. package/src/server-data-source.test.ts +289 -289
  46. package/src/server-data-source.ts +413 -413
  47. package/src/sparkline.test.ts +68 -68
  48. package/src/sparkline.ts +169 -169
  49. package/src/spreadsheet.test.ts +489 -489
  50. package/src/spreadsheet.ts +304 -304
  51. package/src/static-functions.ts +11 -11
  52. package/src/subscribe.ts +38 -38
  53. package/src/svgrid-wrapper.types.ts +439 -439
  54. package/src/svgrid.behavior.test.ts +706 -706
  55. package/src/svgrid.features.test.ts +157 -157
  56. package/src/svgrid.new-features.wrapper.test.ts +251 -251
  57. package/src/svgrid.wrapper.test.ts +40 -40
  58. package/src/themes/index.ts +159 -159
  59. package/src/virtualization/column-virtualizer.test.ts +27 -27
  60. package/src/virtualization/column-virtualizer.ts +30 -30
  61. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  62. package/src/virtualization/types.ts +30 -30
  63. package/src/virtualization/virtualizer.test.ts +47 -47
  64. package/src/virtualization/virtualizer.ts +296 -296
@@ -1,104 +1,104 @@
1
- import { describe, expect, it, vi } from 'vitest'
2
- import {
3
- broadcastChannelTransport,
4
- createCollaboration,
5
- type CollabMessage,
6
- type CollabTransport,
7
- } from './collaboration'
8
-
9
- /** An in-memory bus so two controllers can talk in a test. */
10
- function makeBus() {
11
- const handlers = new Set<(m: CollabMessage) => void>()
12
- const transportFor = (): CollabTransport => ({
13
- post: (msg) => {
14
- // deliver async-ish to every subscriber except none (real channels echo
15
- // to other tabs, not self; our controllers ignore their own id anyway).
16
- for (const h of [...handlers]) h(msg)
17
- },
18
- subscribe: (h) => {
19
- handlers.add(h)
20
- return () => handlers.delete(h)
21
- },
22
- })
23
- return { transportFor }
24
- }
25
-
26
- const userA = { id: 'a', name: 'Ada', color: '#f00' }
27
- const userB = { id: 'b', name: 'Bob', color: '#00f' }
28
-
29
- describe('createCollaboration', () => {
30
- it('peers see each other after hello', () => {
31
- const bus = makeBus()
32
- const a = createCollaboration({ user: userA, transport: bus.transportFor() })
33
- const b = createCollaboration({ user: userB, transport: bus.transportFor() })
34
- // A is announced; B's hello reaches A; A replies presence.
35
- expect(a.peers().map((p) => p.id)).toEqual(['b'])
36
- expect(b.peers().map((p) => p.id)).toEqual(['a'])
37
- a.dispose()
38
- b.dispose()
39
- })
40
-
41
- it('closes the transport channel on dispose (#82)', () => {
42
- // The controller now forwards dispose to the transport...
43
- const disposeSpy = vi.fn()
44
- const collab = createCollaboration({
45
- user: userA,
46
- transport: { post: vi.fn(), subscribe: () => () => {}, dispose: disposeSpy },
47
- })
48
- collab.dispose()
49
- expect(disposeSpy).toHaveBeenCalledTimes(1)
50
-
51
- // ...and broadcastChannelTransport.dispose() closes the BroadcastChannel.
52
- const close = vi.fn()
53
- class FakeBroadcastChannel {
54
- postMessage = vi.fn()
55
- addEventListener = vi.fn()
56
- removeEventListener = vi.fn()
57
- close = close
58
- constructor(public name: string) {}
59
- }
60
- const Orig = (globalThis as { BroadcastChannel?: unknown }).BroadcastChannel
61
- ;(globalThis as { BroadcastChannel?: unknown }).BroadcastChannel = FakeBroadcastChannel
62
- try {
63
- broadcastChannelTransport('room').dispose?.()
64
- expect(close).toHaveBeenCalledTimes(1)
65
- } finally {
66
- ;(globalThis as { BroadcastChannel?: unknown }).BroadcastChannel = Orig
67
- }
68
- })
69
-
70
- it('setCell propagates a remote cursor', () => {
71
- const bus = makeBus()
72
- const onPeers = vi.fn()
73
- const a = createCollaboration({ user: userA, transport: bus.transportFor(), onPeersChange: onPeers })
74
- const b = createCollaboration({ user: userB, transport: bus.transportFor() })
75
- b.setCell({ rowId: 'r1', columnId: 'salary' })
76
- const peerB = a.peers().find((p) => p.id === 'b')
77
- expect(peerB?.cell).toEqual({ rowId: 'r1', columnId: 'salary' })
78
- a.dispose()
79
- b.dispose()
80
- })
81
-
82
- it('sendEdit fires onRemoteEdit on the other peer only', () => {
83
- const bus = makeBus()
84
- const aEdits = vi.fn()
85
- const bEdits = vi.fn()
86
- const a = createCollaboration({ user: userA, transport: bus.transportFor(), onRemoteEdit: aEdits })
87
- const b = createCollaboration({ user: userB, transport: bus.transportFor(), onRemoteEdit: bEdits })
88
- a.sendEdit('r2', 'name', 'Grace')
89
- expect(bEdits).toHaveBeenCalledWith(expect.objectContaining({ rowId: 'r2', columnId: 'name', value: 'Grace' }))
90
- expect(aEdits).not.toHaveBeenCalled() // not echoed to self
91
- a.dispose()
92
- b.dispose()
93
- })
94
-
95
- it('bye removes a peer', () => {
96
- const bus = makeBus()
97
- const a = createCollaboration({ user: userA, transport: bus.transportFor() })
98
- const b = createCollaboration({ user: userB, transport: bus.transportFor() })
99
- expect(a.peers()).toHaveLength(1)
100
- b.dispose()
101
- expect(a.peers()).toHaveLength(0)
102
- a.dispose()
103
- })
104
- })
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import {
3
+ broadcastChannelTransport,
4
+ createCollaboration,
5
+ type CollabMessage,
6
+ type CollabTransport,
7
+ } from './collaboration'
8
+
9
+ /** An in-memory bus so two controllers can talk in a test. */
10
+ function makeBus() {
11
+ const handlers = new Set<(m: CollabMessage) => void>()
12
+ const transportFor = (): CollabTransport => ({
13
+ post: (msg) => {
14
+ // deliver async-ish to every subscriber except none (real channels echo
15
+ // to other tabs, not self; our controllers ignore their own id anyway).
16
+ for (const h of [...handlers]) h(msg)
17
+ },
18
+ subscribe: (h) => {
19
+ handlers.add(h)
20
+ return () => handlers.delete(h)
21
+ },
22
+ })
23
+ return { transportFor }
24
+ }
25
+
26
+ const userA = { id: 'a', name: 'Ada', color: '#f00' }
27
+ const userB = { id: 'b', name: 'Bob', color: '#00f' }
28
+
29
+ describe('createCollaboration', () => {
30
+ it('peers see each other after hello', () => {
31
+ const bus = makeBus()
32
+ const a = createCollaboration({ user: userA, transport: bus.transportFor() })
33
+ const b = createCollaboration({ user: userB, transport: bus.transportFor() })
34
+ // A is announced; B's hello reaches A; A replies presence.
35
+ expect(a.peers().map((p) => p.id)).toEqual(['b'])
36
+ expect(b.peers().map((p) => p.id)).toEqual(['a'])
37
+ a.dispose()
38
+ b.dispose()
39
+ })
40
+
41
+ it('closes the transport channel on dispose (#82)', () => {
42
+ // The controller now forwards dispose to the transport...
43
+ const disposeSpy = vi.fn()
44
+ const collab = createCollaboration({
45
+ user: userA,
46
+ transport: { post: vi.fn(), subscribe: () => () => {}, dispose: disposeSpy },
47
+ })
48
+ collab.dispose()
49
+ expect(disposeSpy).toHaveBeenCalledTimes(1)
50
+
51
+ // ...and broadcastChannelTransport.dispose() closes the BroadcastChannel.
52
+ const close = vi.fn()
53
+ class FakeBroadcastChannel {
54
+ postMessage = vi.fn()
55
+ addEventListener = vi.fn()
56
+ removeEventListener = vi.fn()
57
+ close = close
58
+ constructor(public name: string) {}
59
+ }
60
+ const Orig = (globalThis as { BroadcastChannel?: unknown }).BroadcastChannel
61
+ ;(globalThis as { BroadcastChannel?: unknown }).BroadcastChannel = FakeBroadcastChannel
62
+ try {
63
+ broadcastChannelTransport('room').dispose?.()
64
+ expect(close).toHaveBeenCalledTimes(1)
65
+ } finally {
66
+ ;(globalThis as { BroadcastChannel?: unknown }).BroadcastChannel = Orig
67
+ }
68
+ })
69
+
70
+ it('setCell propagates a remote cursor', () => {
71
+ const bus = makeBus()
72
+ const onPeers = vi.fn()
73
+ const a = createCollaboration({ user: userA, transport: bus.transportFor(), onPeersChange: onPeers })
74
+ const b = createCollaboration({ user: userB, transport: bus.transportFor() })
75
+ b.setCell({ rowId: 'r1', columnId: 'salary' })
76
+ const peerB = a.peers().find((p) => p.id === 'b')
77
+ expect(peerB?.cell).toEqual({ rowId: 'r1', columnId: 'salary' })
78
+ a.dispose()
79
+ b.dispose()
80
+ })
81
+
82
+ it('sendEdit fires onRemoteEdit on the other peer only', () => {
83
+ const bus = makeBus()
84
+ const aEdits = vi.fn()
85
+ const bEdits = vi.fn()
86
+ const a = createCollaboration({ user: userA, transport: bus.transportFor(), onRemoteEdit: aEdits })
87
+ const b = createCollaboration({ user: userB, transport: bus.transportFor(), onRemoteEdit: bEdits })
88
+ a.sendEdit('r2', 'name', 'Grace')
89
+ expect(bEdits).toHaveBeenCalledWith(expect.objectContaining({ rowId: 'r2', columnId: 'name', value: 'Grace' }))
90
+ expect(aEdits).not.toHaveBeenCalled() // not echoed to self
91
+ a.dispose()
92
+ b.dispose()
93
+ })
94
+
95
+ it('bye removes a peer', () => {
96
+ const bus = makeBus()
97
+ const a = createCollaboration({ user: userA, transport: bus.transportFor() })
98
+ const b = createCollaboration({ user: userB, transport: bus.transportFor() })
99
+ expect(a.peers()).toHaveLength(1)
100
+ b.dispose()
101
+ expect(a.peers()).toHaveLength(0)
102
+ a.dispose()
103
+ })
104
+ })
@@ -1,167 +1,167 @@
1
- /**
2
- * Real-time collaboration. Presence (who else is here + where their cursor
3
- * is) and live edits (a change in one client lands in every other), over a
4
- * pluggable transport. The transport is the only thing tied to infrastructure
5
- * - swap `broadcastChannelTransport` (same-browser tabs, zero backend) for a
6
- * WebSocket / WebRTC / CRDT adapter and the controller is unchanged.
7
- *
8
- * This is also the natural "multiple agents on one grid" substrate: an AI
9
- * agent is just another peer posting `edit` messages.
10
- */
11
- export type CollabUser = {
12
- id: string
13
- name: string
14
- /** A CSS color for this user's cursor / avatar. */
15
- color: string
16
- }
17
-
18
- export type CollabCell = { rowId: string; columnId: string }
19
-
20
- export type CollabPresence = CollabUser & {
21
- cell: CollabCell | null
22
- /** Last time we heard from this peer (ms). Used to prune the gone. */
23
- ts: number
24
- }
25
-
26
- export type CollabMessage =
27
- | { kind: 'hello'; user: CollabUser }
28
- | { kind: 'presence'; user: CollabUser; cell: CollabCell | null }
29
- | { kind: 'edit'; user: CollabUser; rowId: string; columnId: string; value: unknown }
30
- | { kind: 'bye'; userId: string }
31
-
32
- export type CollabTransport = {
33
- post(msg: CollabMessage): void
34
- subscribe(handler: (msg: CollabMessage) => void): () => void
35
- /** Release any underlying resource (e.g. a BroadcastChannel). Optional. */
36
- dispose?(): void
37
- }
38
-
39
- export type Collaboration = {
40
- /** Broadcast where this user's cursor is (or null when it leaves). */
41
- setCell(cell: CollabCell | null): void
42
- /** Broadcast a cell edit to every peer. */
43
- sendEdit(rowId: string, columnId: string, value: unknown): void
44
- /** The peers currently present (excludes self). */
45
- peers(): CollabPresence[]
46
- dispose(): void
47
- }
48
-
49
- export type CollaborationOptions = {
50
- user: CollabUser
51
- transport: CollabTransport
52
- /** Fired (with self excluded) whenever the peer set or a cursor changes. */
53
- onPeersChange?: (peers: CollabPresence[]) => void
54
- /** Fired when another user edits a cell - apply it to your data. */
55
- onRemoteEdit?: (edit: {
56
- rowId: string
57
- columnId: string
58
- value: unknown
59
- user: CollabUser
60
- }) => void
61
- /** Drop peers we haven't heard from in this many ms. Default 15000. */
62
- peerTimeoutMs?: number
63
- }
64
-
65
- /** BroadcastChannel transport - live across tabs of the same browser, no
66
- * backend. No-ops where BroadcastChannel is unavailable (SSR / old env). */
67
- export function broadcastChannelTransport(name: string): CollabTransport {
68
- const available = typeof BroadcastChannel !== 'undefined'
69
- const channel = available ? new BroadcastChannel(name) : null
70
- return {
71
- post(msg) {
72
- channel?.postMessage(msg)
73
- },
74
- subscribe(handler) {
75
- if (!channel) return () => {}
76
- const listener = (e: MessageEvent) => handler(e.data as CollabMessage)
77
- channel.addEventListener('message', listener)
78
- return () => channel.removeEventListener('message', listener)
79
- },
80
- dispose() {
81
- channel?.close()
82
- },
83
- }
84
- }
85
-
86
- export function createCollaboration(options: CollaborationOptions): Collaboration {
87
- const { user, transport } = options
88
- const timeout = options.peerTimeoutMs ?? 15_000
89
- const peers = new Map<string, CollabPresence>()
90
- let ownCell: CollabCell | null = null
91
- let disposed = false
92
-
93
- const list = () => [...peers.values()].sort((a, b) => a.name.localeCompare(b.name))
94
- const notify = () => options.onPeersChange?.(list())
95
-
96
- function upsert(u: CollabUser, cell: CollabCell | null) {
97
- if (u.id === user.id) return
98
- peers.set(u.id, { ...u, cell, ts: Date.now() })
99
- notify()
100
- }
101
-
102
- const unsubscribe = transport.subscribe((msg) => {
103
- if (disposed) return
104
- switch (msg.kind) {
105
- case 'hello':
106
- upsert(msg.user, null)
107
- // Let the newcomer know we're here (and where our cursor is).
108
- transport.post({ kind: 'presence', user, cell: ownCell })
109
- break
110
- case 'presence':
111
- upsert(msg.user, msg.cell)
112
- break
113
- case 'edit':
114
- upsert(msg.user, { rowId: msg.rowId, columnId: msg.columnId })
115
- if (msg.user.id !== user.id) {
116
- options.onRemoteEdit?.({
117
- rowId: msg.rowId,
118
- columnId: msg.columnId,
119
- value: msg.value,
120
- user: msg.user,
121
- })
122
- }
123
- break
124
- case 'bye':
125
- if (peers.delete(msg.userId)) notify()
126
- break
127
- }
128
- })
129
-
130
- // Prune peers that went silent (closed tab without a `bye`).
131
- const pruneTimer =
132
- typeof setInterval !== 'undefined'
133
- ? setInterval(() => {
134
- const now = Date.now()
135
- let changed = false
136
- for (const [id, p] of peers) {
137
- if (now - p.ts > timeout) {
138
- peers.delete(id)
139
- changed = true
140
- }
141
- }
142
- if (changed) notify()
143
- }, Math.max(1000, timeout / 3))
144
- : null
145
-
146
- // Announce ourselves.
147
- transport.post({ kind: 'hello', user })
148
-
149
- return {
150
- setCell(cell) {
151
- ownCell = cell
152
- transport.post({ kind: 'presence', user, cell })
153
- },
154
- sendEdit(rowId, columnId, value) {
155
- transport.post({ kind: 'edit', user, rowId, columnId, value })
156
- },
157
- peers: list,
158
- dispose() {
159
- if (disposed) return
160
- disposed = true
161
- transport.post({ kind: 'bye', userId: user.id })
162
- unsubscribe()
163
- if (pruneTimer) clearInterval(pruneTimer)
164
- transport.dispose?.() // release the channel (BroadcastChannel etc.)
165
- },
166
- }
167
- }
1
+ /**
2
+ * Real-time collaboration. Presence (who else is here + where their cursor
3
+ * is) and live edits (a change in one client lands in every other), over a
4
+ * pluggable transport. The transport is the only thing tied to infrastructure
5
+ * - swap `broadcastChannelTransport` (same-browser tabs, zero backend) for a
6
+ * WebSocket / WebRTC / CRDT adapter and the controller is unchanged.
7
+ *
8
+ * This is also the natural "multiple agents on one grid" substrate: an AI
9
+ * agent is just another peer posting `edit` messages.
10
+ */
11
+ export type CollabUser = {
12
+ id: string
13
+ name: string
14
+ /** A CSS color for this user's cursor / avatar. */
15
+ color: string
16
+ }
17
+
18
+ export type CollabCell = { rowId: string; columnId: string }
19
+
20
+ export type CollabPresence = CollabUser & {
21
+ cell: CollabCell | null
22
+ /** Last time we heard from this peer (ms). Used to prune the gone. */
23
+ ts: number
24
+ }
25
+
26
+ export type CollabMessage =
27
+ | { kind: 'hello'; user: CollabUser }
28
+ | { kind: 'presence'; user: CollabUser; cell: CollabCell | null }
29
+ | { kind: 'edit'; user: CollabUser; rowId: string; columnId: string; value: unknown }
30
+ | { kind: 'bye'; userId: string }
31
+
32
+ export type CollabTransport = {
33
+ post(msg: CollabMessage): void
34
+ subscribe(handler: (msg: CollabMessage) => void): () => void
35
+ /** Release any underlying resource (e.g. a BroadcastChannel). Optional. */
36
+ dispose?(): void
37
+ }
38
+
39
+ export type Collaboration = {
40
+ /** Broadcast where this user's cursor is (or null when it leaves). */
41
+ setCell(cell: CollabCell | null): void
42
+ /** Broadcast a cell edit to every peer. */
43
+ sendEdit(rowId: string, columnId: string, value: unknown): void
44
+ /** The peers currently present (excludes self). */
45
+ peers(): CollabPresence[]
46
+ dispose(): void
47
+ }
48
+
49
+ export type CollaborationOptions = {
50
+ user: CollabUser
51
+ transport: CollabTransport
52
+ /** Fired (with self excluded) whenever the peer set or a cursor changes. */
53
+ onPeersChange?: (peers: CollabPresence[]) => void
54
+ /** Fired when another user edits a cell - apply it to your data. */
55
+ onRemoteEdit?: (edit: {
56
+ rowId: string
57
+ columnId: string
58
+ value: unknown
59
+ user: CollabUser
60
+ }) => void
61
+ /** Drop peers we haven't heard from in this many ms. Default 15000. */
62
+ peerTimeoutMs?: number
63
+ }
64
+
65
+ /** BroadcastChannel transport - live across tabs of the same browser, no
66
+ * backend. No-ops where BroadcastChannel is unavailable (SSR / old env). */
67
+ export function broadcastChannelTransport(name: string): CollabTransport {
68
+ const available = typeof BroadcastChannel !== 'undefined'
69
+ const channel = available ? new BroadcastChannel(name) : null
70
+ return {
71
+ post(msg) {
72
+ channel?.postMessage(msg)
73
+ },
74
+ subscribe(handler) {
75
+ if (!channel) return () => {}
76
+ const listener = (e: MessageEvent) => handler(e.data as CollabMessage)
77
+ channel.addEventListener('message', listener)
78
+ return () => channel.removeEventListener('message', listener)
79
+ },
80
+ dispose() {
81
+ channel?.close()
82
+ },
83
+ }
84
+ }
85
+
86
+ export function createCollaboration(options: CollaborationOptions): Collaboration {
87
+ const { user, transport } = options
88
+ const timeout = options.peerTimeoutMs ?? 15_000
89
+ const peers = new Map<string, CollabPresence>()
90
+ let ownCell: CollabCell | null = null
91
+ let disposed = false
92
+
93
+ const list = () => [...peers.values()].sort((a, b) => a.name.localeCompare(b.name))
94
+ const notify = () => options.onPeersChange?.(list())
95
+
96
+ function upsert(u: CollabUser, cell: CollabCell | null) {
97
+ if (u.id === user.id) return
98
+ peers.set(u.id, { ...u, cell, ts: Date.now() })
99
+ notify()
100
+ }
101
+
102
+ const unsubscribe = transport.subscribe((msg) => {
103
+ if (disposed) return
104
+ switch (msg.kind) {
105
+ case 'hello':
106
+ upsert(msg.user, null)
107
+ // Let the newcomer know we're here (and where our cursor is).
108
+ transport.post({ kind: 'presence', user, cell: ownCell })
109
+ break
110
+ case 'presence':
111
+ upsert(msg.user, msg.cell)
112
+ break
113
+ case 'edit':
114
+ upsert(msg.user, { rowId: msg.rowId, columnId: msg.columnId })
115
+ if (msg.user.id !== user.id) {
116
+ options.onRemoteEdit?.({
117
+ rowId: msg.rowId,
118
+ columnId: msg.columnId,
119
+ value: msg.value,
120
+ user: msg.user,
121
+ })
122
+ }
123
+ break
124
+ case 'bye':
125
+ if (peers.delete(msg.userId)) notify()
126
+ break
127
+ }
128
+ })
129
+
130
+ // Prune peers that went silent (closed tab without a `bye`).
131
+ const pruneTimer =
132
+ typeof setInterval !== 'undefined'
133
+ ? setInterval(() => {
134
+ const now = Date.now()
135
+ let changed = false
136
+ for (const [id, p] of peers) {
137
+ if (now - p.ts > timeout) {
138
+ peers.delete(id)
139
+ changed = true
140
+ }
141
+ }
142
+ if (changed) notify()
143
+ }, Math.max(1000, timeout / 3))
144
+ : null
145
+
146
+ // Announce ourselves.
147
+ transport.post({ kind: 'hello', user })
148
+
149
+ return {
150
+ setCell(cell) {
151
+ ownCell = cell
152
+ transport.post({ kind: 'presence', user, cell })
153
+ },
154
+ sendEdit(rowId, columnId, value) {
155
+ transport.post({ kind: 'edit', user, rowId, columnId, value })
156
+ },
157
+ peers: list,
158
+ dispose() {
159
+ if (disposed) return
160
+ disposed = true
161
+ transport.post({ kind: 'bye', userId: user.id })
162
+ unsubscribe()
163
+ if (pruneTimer) clearInterval(pruneTimer)
164
+ transport.dispose?.() // release the channel (BroadcastChannel etc.)
165
+ },
166
+ }
167
+ }
@@ -1,30 +1,30 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { createCoreRowModel, createSortedRowModel, createSvGrid, sortFns, tableFeatures } from './index'
3
-
4
- describe('core performance smoke', () => {
5
- it('handles large synthetic shape with bounded row model call', () => {
6
- const columns = Array.from({ length: 50 }, (_, index) => ({
7
- field: `c${index}`,
8
- editorType: 'number' as const,
9
- }))
10
- const data = Array.from({ length: 5000 }, (_, rowIndex) => {
11
- const row: Record<string, number> = {}
12
- for (let col = 0; col < 50; col += 1) row[`c${col}`] = rowIndex + col
13
- return row
14
- })
15
- const grid = createSvGrid({
16
- _features: tableFeatures({}),
17
- _rowModels: {
18
- coreRowModel: createCoreRowModel(),
19
- sortedRowModel: createSortedRowModel(sortFns),
20
- },
21
- columns,
22
- data,
23
- state: { sorting: [{ id: 'c0', desc: true }, { id: 'c1', desc: false }] },
24
- })
25
-
26
- const rows = grid.getRowModel().rows
27
- expect(rows.length).toBe(5000)
28
- expect(rows[0]?.getCellValueByColumnId('c0')).toBe(4999)
29
- })
30
- })
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createCoreRowModel, createSortedRowModel, createSvGrid, sortFns, tableFeatures } from './index'
3
+
4
+ describe('core performance smoke', () => {
5
+ it('handles large synthetic shape with bounded row model call', () => {
6
+ const columns = Array.from({ length: 50 }, (_, index) => ({
7
+ field: `c${index}`,
8
+ editorType: 'number' as const,
9
+ }))
10
+ const data = Array.from({ length: 5000 }, (_, rowIndex) => {
11
+ const row: Record<string, number> = {}
12
+ for (let col = 0; col < 50; col += 1) row[`c${col}`] = rowIndex + col
13
+ return row
14
+ })
15
+ const grid = createSvGrid({
16
+ _features: tableFeatures({}),
17
+ _rowModels: {
18
+ coreRowModel: createCoreRowModel(),
19
+ sortedRowModel: createSortedRowModel(sortFns),
20
+ },
21
+ columns,
22
+ data,
23
+ state: { sorting: [{ id: 'c0', desc: true }, { id: 'c1', desc: false }] },
24
+ })
25
+
26
+ const rows = grid.getRowModel().rows
27
+ expect(rows.length).toBe(5000)
28
+ expect(rows[0]?.getCellValueByColumnId('c0')).toBe(4999)
29
+ })
30
+ })