wireweave 0.3.21 → 0.3.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wireweave",
3
- "version": "0.3.21",
3
+ "version": "0.3.23",
4
4
  "description": "nostr + webrtc voice + data SDK. networking layer for 247420 projects.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/bans.js CHANGED
@@ -6,10 +6,11 @@ export class Bans extends EventTarget {
6
6
  if (!relayPool) throw new Error('Bans: relayPool required');
7
7
  this.pool = relayPool; this.auth = auth; this.roles = roles;
8
8
  this.store = new Map();
9
- this.sub = null;
9
+ this.subs = new Map();
10
10
  }
11
11
 
12
12
  isBanned(serverId, pubkey) { return !!(this.store.get(serverId)?.banned || []).includes(pubkey); }
13
+ isKicked(serverId, pubkey) { return !!(this.store.get(serverId)?.kicked || []).includes(pubkey); }
13
14
 
14
15
  isTimedOut(serverId, pubkey) {
15
16
  const t = this.store.get(serverId)?.timeouts?.[pubkey];
@@ -51,12 +52,13 @@ export class Bans extends EventTarget {
51
52
  }
52
53
 
53
54
  subscribe(serverId) {
54
- if (this.sub) { this.pool.unsubscribe(this.sub); this.sub = null; }
55
+ if (this.subs.has(serverId)) return;
55
56
  if (!serverId) return;
56
57
  const creator = serverId.split(':')[0];
57
58
  if (!creator) return;
58
- this.sub = 'bans-' + serverId;
59
- this.pool.subscribe(this.sub,
59
+ const subId = 'bans-' + serverId;
60
+ this.subs.set(serverId, subId);
61
+ this.pool.subscribe(subId,
60
62
  [{ kinds: [30078], authors: [creator], '#server': [serverId] }],
61
63
  (event) => {
62
64
  if (event.pubkey !== creator) return;
@@ -64,11 +66,14 @@ export class Bans extends EventTarget {
64
66
  const dTag = event.tags.find(t => t[0] === 'd');
65
67
  if (!dTag?.[1]) return;
66
68
  const parsed = parseDtag(dTag[1]);
67
- if (!parsed || (parsed.ns !== 'ban' && parsed.ns !== 'timeout')) return;
69
+ if (!parsed || !['ban', 'timeout', 'kick'].includes(parsed.ns)) return;
68
70
  const pubkey = parsed.parts[parsed.parts.length - 1];
69
- const data = this.store.get(serverId) || { banned: [], timeouts: {} };
71
+ const data = this.store.get(serverId) || { banned: [], timeouts: {}, kicked: [] };
70
72
  if (parsed.ns === 'ban' && pubkey && !data.banned.includes(pubkey)) data.banned.push(pubkey);
71
- else if (parsed.ns === 'timeout' && pubkey) {
73
+ else if (parsed.ns === 'kick' && pubkey) {
74
+ data.kicked = data.kicked || [];
75
+ if (!data.kicked.includes(pubkey)) data.kicked.push(pubkey);
76
+ } else if (parsed.ns === 'timeout' && pubkey) {
72
77
  const body = JSON.parse(event.content);
73
78
  if (body.expiry > Math.floor(Date.now() / 1000)) (data.timeouts = data.timeouts || {})[pubkey] = { expiry: body.expiry };
74
79
  else if (data.timeouts?.[pubkey]) delete data.timeouts[pubkey];
@@ -78,6 +83,11 @@ export class Bans extends EventTarget {
78
83
  } catch {}
79
84
  });
80
85
  }
86
+
87
+ unsubscribe(serverId) {
88
+ const subId = this.subs.get(serverId);
89
+ if (subId) { this.pool.unsubscribe(subId); this.subs.delete(serverId); }
90
+ }
81
91
  }
82
92
 
83
93
  export const createBans = (opts) => new Bans(opts);
package/src/channels.js CHANGED
@@ -19,9 +19,10 @@ export class Channels extends EventTarget {
19
19
  this.serverId = ''; this.channels = []; this.categories = [];
20
20
  }
21
21
 
22
- isOwner() { return this.auth.pubkey && this.serverId && this.auth.pubkey === this.serverId.split(':')[0]; }
22
+ isOwner() { return !!(this.auth.pubkey && this.serverId && this.auth.pubkey === this.serverId.split(':')[0]); }
23
23
 
24
24
  load(serverId, onReady) {
25
+ if (this.serverId) this.pool.unsubscribe('channels-' + this.serverId);
25
26
  this.serverId = serverId;
26
27
  this.channels = []; this.categories = [];
27
28
  const ownerPubkey = serverId.split(':')[0];
@@ -62,12 +63,14 @@ export class Channels extends EventTarget {
62
63
  }
63
64
 
64
65
  async create(name, type = 'text', categoryId = 'general') {
65
- this.channels = [...this.channels, { id: 'ch-' + Date.now(), name, type, categoryId, position: this.channels.length }];
66
+ if (!this.isOwner()) throw new Error('owner only');
67
+ this.channels = [...this.channels, { id: 'ch-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6), name, type, categoryId, position: this.channels.length }];
66
68
  await this._publish();
67
69
  this._emit('updated', { channels: this.channels, categories: this.categories });
68
70
  }
69
71
 
70
72
  async rename(id, name) {
73
+ if (!this.isOwner()) throw new Error('owner only');
71
74
  this.channels = this.channels.map(c => c.id === id ? { ...c, name } : c);
72
75
  await this._publish(); this._emit('updated', { channels: this.channels, categories: this.categories });
73
76
  }
@@ -75,38 +78,45 @@ export class Channels extends EventTarget {
75
78
  // Per-channel metadata patch — used for topic, voiceMode, and any future
76
79
  // server-published channel-scoped configuration. Owner-only.
77
80
  async update(id, patch) {
81
+ if (!this.isOwner()) throw new Error('owner only');
78
82
  if (!patch || typeof patch !== 'object') return;
79
83
  this.channels = this.channels.map(c => c.id === id ? { ...c, ...patch } : c);
80
84
  await this._publish(); this._emit('updated', { channels: this.channels, categories: this.categories });
81
85
  }
82
86
 
83
87
  async remove(id) {
88
+ if (!this.isOwner()) throw new Error('owner only');
84
89
  this.channels = this.channels.filter(c => c.id !== id);
85
90
  await this._publish(); this._emit('updated', { channels: this.channels, categories: this.categories });
86
91
  }
87
92
 
88
93
  async createCategory(name) {
89
- this.categories = [...this.categories, { id: 'cat-' + Date.now(), name, position: this.categories.length }];
94
+ if (!this.isOwner()) throw new Error('owner only');
95
+ this.categories = [...this.categories, { id: 'cat-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6), name, position: this.categories.length }];
90
96
  await this._publish(); this._emit('updated', { channels: this.channels, categories: this.categories });
91
97
  }
92
98
 
93
99
  async renameCategory(id, name) {
100
+ if (!this.isOwner()) throw new Error('owner only');
94
101
  this.categories = this.categories.map(c => c.id === id ? { ...c, name } : c);
95
102
  await this._publish(); this._emit('updated', { channels: this.channels, categories: this.categories });
96
103
  }
97
104
 
98
105
  async deleteCategory(id) {
106
+ if (!this.isOwner()) throw new Error('owner only');
99
107
  this.categories = this.categories.filter(c => c.id !== id);
100
108
  this.channels = this.channels.map(c => c.categoryId === id ? { ...c, categoryId: null } : c);
101
109
  await this._publish(); this._emit('updated', { channels: this.channels, categories: this.categories });
102
110
  }
103
111
 
104
112
  async reorder(catId, ids) {
113
+ if (!this.isOwner()) throw new Error('owner only');
105
114
  ids.forEach((chId, idx) => { this.channels = this.channels.map(c => c.id === chId ? { ...c, position: idx, categoryId: catId } : c); });
106
115
  await this._publish(); this._emit('updated', { channels: this.channels, categories: this.categories });
107
116
  }
108
117
 
109
118
  async reorderCategories(ids) {
119
+ if (!this.isOwner()) throw new Error('owner only');
110
120
  ids.forEach((catId, idx) => { this.categories = this.categories.map(c => c.id === catId ? { ...c, position: idx } : c); });
111
121
  await this._publish(); this._emit('updated', { channels: this.channels, categories: this.categories });
112
122
  }
package/src/chat.js CHANGED
@@ -52,6 +52,11 @@ export class Chat extends EventTarget {
52
52
  }
53
53
 
54
54
  async deleteMessage(id) {
55
+ const msg = this.messages.find(m => m.id === id);
56
+ if (!msg) return;
57
+ const { serverId } = this.getChannelContext();
58
+ const isAuthor = msg.userId === this.auth.pubkey;
59
+ if (!isAuthor && !this.isAdmin(serverId)) throw new Error('Cannot delete: not author or admin');
55
60
  const signed = await this.auth.sign({ kind: 5, created_at: Math.floor(Date.now() / 1000), tags: [['e', id]], content: 'deleted' });
56
61
  this.pool.publish(signed);
57
62
  this.messages = this.messages.filter(m => m.id !== id);
package/src/data.js CHANGED
@@ -34,7 +34,8 @@ export class DataSession extends EventTarget {
34
34
  }
35
35
 
36
36
  _initActor() {
37
- const machine = this.fsm.dataMachine || this.fsm.voiceMachine;
37
+ const machine = this.fsm.dataMachine;
38
+ if (!machine) throw new Error('DataSession: fsm.dataMachine missing');
38
39
  this.actor = this.xstate.createActor(machine);
39
40
  this.actor.subscribe((snap) => this.dispatchEvent(new CustomEvent('state', { detail: { value: snap.value } })));
40
41
  this.actor.start();
package/src/message.js CHANGED
@@ -10,7 +10,7 @@ export class MessageBus extends EventTarget {
10
10
  register(type, fn) { this.handlers[type] = fn; }
11
11
 
12
12
  add(text, { audioData = null, userId = null, username = null } = {}) {
13
- const msg = { id: Date.now() + Math.random(), text, time: Date.now(), userId, username, audioData };
13
+ const msg = { id: Date.now().toString(36) + Math.random().toString(36).slice(2), text, time: Date.now(), userId, username, audioData };
14
14
  this.messages = [...this.messages, msg];
15
15
  if (this.messages.length > this.max) this.messages = this.messages.slice(-this.max);
16
16
  this.dispatchEvent(new CustomEvent('message', { detail: msg }));
package/src/roles.js CHANGED
@@ -6,7 +6,7 @@ export class Roles extends EventTarget {
6
6
  if (!relayPool || !auth) throw new Error('Roles: deps required');
7
7
  this.pool = relayPool; this.auth = auth;
8
8
  this.store = new Map();
9
- this.sub = null;
9
+ this.subs = new Map();
10
10
  }
11
11
 
12
12
  _creatorOf(serverId) { return serverId ? serverId.split(':')[0] : null; }
@@ -39,12 +39,13 @@ export class Roles extends EventTarget {
39
39
  }
40
40
 
41
41
  subscribe(serverId) {
42
- if (this.sub) { this.pool.unsubscribe(this.sub); this.sub = null; }
42
+ if (this.subs.has(serverId)) return;
43
43
  if (!serverId) return;
44
44
  const creator = this._creatorOf(serverId);
45
45
  if (!creator) return;
46
- this.sub = 'roles-' + serverId;
47
- this.pool.subscribe(this.sub,
46
+ const subId = 'roles-' + serverId;
47
+ this.subs.set(serverId, subId);
48
+ this.pool.subscribe(subId,
48
49
  [{ kinds: [30078], authors: [creator], '#d': [dtag('roles', serverId)] }],
49
50
  (event) => {
50
51
  if (event.pubkey !== creator) return;
@@ -55,6 +56,11 @@ export class Roles extends EventTarget {
55
56
  } catch {}
56
57
  });
57
58
  }
59
+
60
+ unsubscribe(serverId) {
61
+ const subId = this.subs.get(serverId);
62
+ if (subId) { this.pool.unsubscribe(subId); this.subs.delete(serverId); }
63
+ }
58
64
  }
59
65
 
60
66
  export const createRoles = (opts) => new Roles(opts);
package/src/settings.js CHANGED
@@ -9,7 +9,7 @@ export class Settings extends EventTarget {
9
9
  if (!relayPool || !auth || !roles) throw new Error('Settings: deps required');
10
10
  this.pool = relayPool; this.auth = auth; this.roles = roles;
11
11
  this.store = new Map();
12
- this.sub = null;
12
+ this.subs = new Map();
13
13
  }
14
14
 
15
15
  getBitrate(serverId) { return this.store.get(serverId)?.opusBitrate || 24000; }
@@ -57,18 +57,24 @@ export class Settings extends EventTarget {
57
57
  }
58
58
 
59
59
  subscribe(serverId) {
60
- if (this.sub) { this.pool.unsubscribe(this.sub); this.sub = null; }
60
+ if (this.subs.has(serverId)) return;
61
61
  if (!serverId) return;
62
62
  const creator = serverId.split(':')[0];
63
63
  if (!creator) return;
64
- this.sub = 'settings-' + serverId;
65
- this.pool.subscribe(this.sub,
64
+ const subId = 'settings-' + serverId;
65
+ this.subs.set(serverId, subId);
66
+ this.pool.subscribe(subId,
66
67
  [{ kinds: [30078], authors: [creator], '#d': [dtag('settings', serverId)] }],
67
68
  (event) => {
68
69
  if (event.pubkey !== creator) return;
69
70
  try { this.store.set(serverId, JSON.parse(event.content)); this.dispatchEvent(new CustomEvent('updated', { detail: { serverId, next: this.store.get(serverId) } })); } catch {}
70
71
  });
71
72
  }
73
+
74
+ unsubscribe(serverId) {
75
+ const subId = this.subs.get(serverId);
76
+ if (subId) { this.pool.unsubscribe(subId); this.subs.delete(serverId); }
77
+ }
72
78
  }
73
79
 
74
80
  export const createSettings = (opts) => new Settings(opts);
package/src/wireweave.js CHANGED
@@ -12,6 +12,7 @@ import { createSettings } from './settings.js';
12
12
  import { createMedia } from './media.js';
13
13
  import { createPages } from './pages.js';
14
14
  import { createDM } from './dm.js';
15
+ import { createDataSession } from './data.js';
15
16
  import { register } from './debug.js';
16
17
 
17
18
  export const createWireweave = ({
@@ -86,12 +87,24 @@ export const createWireweave = ({
86
87
  return dm;
87
88
  };
88
89
 
90
+ // DataSession is lazy for the same reason as DM: requires xstate and FSM.
91
+ // onSwitch accumulates subscriptions across all visited servers (idempotent
92
+ // Map pattern) — no unsubscribe on server switch by design so offline data
93
+ // from prior servers remains cached.
94
+ let data = null;
95
+ const ensureData = ({ room = '', displayName = 'Guest', namespace = '' } = {}) => {
96
+ if (!data) data = createDataSession({ fsm, xstate, relayPool: pool, auth, namespace });
97
+ return data;
98
+ };
99
+
89
100
  const api = {
90
101
  pool, auth, fsm, message, bans, roles, settings, pages, media, channels, servers, chat,
91
102
  get voice() { return voice; },
92
103
  ensureVoice,
93
104
  get dm() { return dm; },
94
105
  ensureDM,
106
+ get data() { return data; },
107
+ ensureData,
95
108
  setCurrentChannel,
96
109
  get currentChannelId() { return currentChannelId; },
97
110
  get currentServerId() { return servers.currentServerId; }