@shoru/kitten 0.0.5 → 0.1.0

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.
@@ -1,95 +1,185 @@
1
- import { EVENTS } from './config.js';
2
-
3
- const plugins = new Map();
4
-
5
- const eventCounts = new Map([...EVENTS].map(e => [e, 0]));
6
-
7
- const buckets = Object.fromEntries(
8
- [...EVENTS].map(e => [e, { auto: new Map(), match: new Map() }])
9
- );
10
-
11
- // Plugin Map Operations
12
-
13
- export function getPlugin(id) {
14
- return plugins.get(id);
15
- }
16
-
17
- export function setPlugin(id, plugin) {
18
- plugins.set(id, plugin);
19
- }
20
-
21
- export function deletePlugin(id) {
22
- plugins.delete(id);
23
- }
24
-
25
- export function getAllPlugins() {
26
- return plugins;
27
- }
28
-
29
- export function getPluginCount() {
30
- return plugins.size;
31
- }
32
-
33
- // Bucket Operations
34
-
35
- export function getBucket(event) {
36
- return buckets[event];
37
- }
38
-
39
- export function getEventCounts() {
40
- return eventCounts;
41
- }
42
-
43
- export function registerToBuckets(id, plugin) {
44
- const key = plugin._meta.matchers ? 'match' : 'auto';
45
-
46
- for (const event of plugin._meta.events) {
47
- const bucket = buckets[event]?.[key];
48
- if (bucket && !bucket.has(id)) {
49
- bucket.set(id, plugin);
50
- eventCounts.set(event, (eventCounts.get(event) ?? 0) + 1);
51
- }
52
- }
53
- }
54
-
55
- export function unregisterFromBuckets(id) {
56
- const plugin = plugins.get(id);
57
- const events = plugin?._meta?.events ?? [];
58
-
59
- for (const event of events) {
60
- const bucket = buckets[event];
61
- if (bucket?.auto.delete(id) || bucket?.match.delete(id)) {
62
- eventCounts.set(event, Math.max(0, (eventCounts.get(event) ?? 1) - 1));
63
- }
64
- }
65
- }
66
-
67
- export function unloadByFilePath(filePath) {
68
- const idsToUnload = [];
69
-
70
- for (const [id, plugin] of plugins) {
71
- if (plugin._meta?.filePath === filePath) {
72
- idsToUnload.push(id);
73
- }
74
- }
75
-
76
- for (const id of idsToUnload) {
77
- unregisterFromBuckets(id);
78
- plugins.delete(id);
79
- }
80
-
81
- return idsToUnload.length;
82
- }
83
-
84
- export function clear() {
85
- plugins.clear();
86
-
87
- for (const bucket of Object.values(buckets)) {
88
- bucket.auto.clear();
89
- bucket.match.clear();
90
- }
91
-
92
- for (const event of EVENTS) {
93
- eventCounts.set(event, 0);
94
- }
1
+ import path from 'path';
2
+ import { EVENTS } from './config.js';
3
+
4
+ const plugins = new Map();
5
+
6
+ const eventCounts = new Map([...EVENTS].map(e => [e, 0]));
7
+
8
+ const createBucket = () => {
9
+ const allMatch = new Map();
10
+ return {
11
+ auto: new Map(),
12
+ commandMap: new Map(),
13
+ prefixlessMap: new Map(),
14
+ regexList: new Map(),
15
+ allMatch,
16
+ get match() {
17
+ return allMatch;
18
+ },
19
+ };
20
+ };
21
+
22
+ const buckets = Object.fromEntries(
23
+ [...EVENTS].map(e => [e, createBucket()])
24
+ );
25
+
26
+ // Plugin Map Operations
27
+
28
+ export function getPlugin(id) {
29
+ return plugins.get(id);
30
+ }
31
+
32
+ export function setPlugin(id, plugin) {
33
+ plugins.set(id, plugin);
34
+ }
35
+
36
+ export function deletePlugin(id) {
37
+ plugins.delete(id);
38
+ }
39
+
40
+ export function getAllPlugins() {
41
+ return plugins;
42
+ }
43
+
44
+ export function getPluginCount() {
45
+ return plugins.size;
46
+ }
47
+
48
+ // Bucket Operations
49
+
50
+ export function getBucket(event) {
51
+ return buckets[event];
52
+ }
53
+
54
+ export function getEventCounts() {
55
+ return eventCounts;
56
+ }
57
+
58
+ export function registerToBuckets(id, plugin) {
59
+ const matchers = plugin._meta?.matchers;
60
+ const isMatch = Boolean(matchers);
61
+
62
+ for (const event of plugin._meta.events) {
63
+ const bucket = buckets[event];
64
+ if (!bucket) continue;
65
+
66
+ if (!isMatch) {
67
+ if (!bucket.auto.has(id)) {
68
+ eventCounts.set(event, (eventCounts.get(event) ?? 0) + 1);
69
+ }
70
+ bucket.auto.set(id, plugin);
71
+ } else {
72
+ if (!bucket.allMatch.has(id)) {
73
+ eventCounts.set(event, (eventCounts.get(event) ?? 0) + 1);
74
+ }
75
+ bucket.allMatch.set(id, plugin);
76
+
77
+ // Index exact string commands with case normalization
78
+ if (matchers.strings && matchers.strings.length > 0) {
79
+ for (const rawCmd of matchers.strings) {
80
+ const normCmd = rawCmd.toLowerCase();
81
+
82
+ if (matchers.prefixes && matchers.prefixes.size > 0) {
83
+ for (const rawPrefix of matchers.prefixes) {
84
+ const normPrefix = rawPrefix.toLowerCase();
85
+ const key = `${normPrefix}${normCmd}`;
86
+ let list = bucket.commandMap.get(key);
87
+ if (!list) {
88
+ list = [];
89
+ bucket.commandMap.set(key, list);
90
+ }
91
+ list.push({ id, plugin, match: normCmd, prefix: normPrefix });
92
+ }
93
+ } else {
94
+ let list = bucket.prefixlessMap.get(normCmd);
95
+ if (!list) {
96
+ list = [];
97
+ bucket.prefixlessMap.set(normCmd, list);
98
+ }
99
+ list.push({ id, plugin, match: normCmd, prefix: null });
100
+ }
101
+ }
102
+ }
103
+
104
+ // Index regex matchers
105
+ if (matchers.regexes && matchers.regexes.length > 0) {
106
+ bucket.regexList.set(id, { plugin, regexes: matchers.regexes });
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ export function unregisterFromBuckets(id) {
113
+ const plugin = plugins.get(id);
114
+ const events = plugin?._meta?.events ?? [];
115
+
116
+ for (const event of events) {
117
+ const bucket = buckets[event];
118
+ if (!bucket) continue;
119
+
120
+ let removed = false;
121
+ if (bucket.auto.delete(id)) removed = true;
122
+ if (bucket.allMatch.delete(id)) removed = true;
123
+
124
+ // Clean up commandMap
125
+ for (const [key, list] of bucket.commandMap) {
126
+ const filtered = list.filter(item => item.id !== id);
127
+ if (filtered.length === 0) {
128
+ bucket.commandMap.delete(key);
129
+ } else {
130
+ bucket.commandMap.set(key, filtered);
131
+ }
132
+ }
133
+
134
+ // Clean up prefixlessMap
135
+ for (const [key, list] of bucket.prefixlessMap) {
136
+ const filtered = list.filter(item => item.id !== id);
137
+ if (filtered.length === 0) {
138
+ bucket.prefixlessMap.delete(key);
139
+ } else {
140
+ bucket.prefixlessMap.set(key, filtered);
141
+ }
142
+ }
143
+
144
+ // Clean up regexList
145
+ bucket.regexList.delete(id);
146
+
147
+ if (removed) {
148
+ eventCounts.set(event, Math.max(0, (eventCounts.get(event) ?? 1) - 1));
149
+ }
150
+ }
151
+ }
152
+
153
+ export function unloadByFilePath(filePath) {
154
+ const normalizedTarget = path.resolve(filePath);
155
+ const idsToUnload = [];
156
+
157
+ for (const [id, plugin] of plugins) {
158
+ if (plugin._meta?.filePath && path.resolve(plugin._meta.filePath) === normalizedTarget) {
159
+ idsToUnload.push(id);
160
+ }
161
+ }
162
+
163
+ for (const id of idsToUnload) {
164
+ unregisterFromBuckets(id);
165
+ plugins.delete(id);
166
+ }
167
+
168
+ return idsToUnload.length;
169
+ }
170
+
171
+ export function clear() {
172
+ plugins.clear();
173
+
174
+ for (const bucket of Object.values(buckets)) {
175
+ bucket.auto.clear();
176
+ bucket.allMatch.clear();
177
+ bucket.commandMap.clear();
178
+ bucket.prefixlessMap.clear();
179
+ bucket.regexList.clear();
180
+ }
181
+
182
+ for (const event of EVENTS) {
183
+ eventCounts.set(event, 0);
184
+ }
95
185
  }
@@ -50,32 +50,34 @@ export function clearTimers() {
50
50
  }
51
51
 
52
52
  function scheduleHMR(filePath, type) {
53
- clearTimeout(debounceTimers.get(filePath));
53
+ const resolved = path.resolve(filePath);
54
+ clearTimeout(debounceTimers.get(resolved));
54
55
 
55
56
  debounceTimers.set(
56
- filePath,
57
+ resolved,
57
58
  setTimeout(() => {
58
- debounceTimers.delete(filePath);
59
- executeHMR(filePath, type);
59
+ debounceTimers.delete(resolved);
60
+ executeHMR(resolved, type);
60
61
  }, debounceMs)
61
62
  );
62
63
  }
63
64
 
64
65
  async function executeHMR(filePath, type) {
65
- const rel = path.relative(PLUGIN_DIR, filePath);
66
+ const resolved = path.resolve(filePath);
67
+ const rel = path.relative(PLUGIN_DIR, resolved);
66
68
 
67
69
  try {
68
- await getLock(filePath).runExclusive(async () => {
70
+ await getLock(resolved).runExclusive(async () => {
69
71
  if (type === 'unlink') {
70
- const count = unloadByFilePath(filePath);
72
+ const count = unloadByFilePath(resolved);
71
73
  logger.info(`[HMR] Unloaded: ${rel} (${count})`);
72
74
  } else {
73
75
  // Load new plugins without registering
74
- const parent = getParentFolder(path.dirname(filePath));
75
- const loaded = await loadFile(filePath, parent, false);
76
+ const parent = getParentFolder(path.dirname(resolved));
77
+ const loaded = await loadFile(resolved, parent, false);
76
78
 
77
79
  // Remove old plugins for this file
78
- unloadByFilePath(filePath);
80
+ unloadByFilePath(resolved);
79
81
 
80
82
  // Register newly loaded plugins
81
83
  for (const [id, plugin] of loaded) {
@@ -93,7 +95,7 @@ async function executeHMR(filePath, type) {
93
95
  handleError(`[HMR:${rel}] Failed:`, err);
94
96
  } finally {
95
97
  if (type === 'unlink') {
96
- deleteLockIfUnused(filePath);
98
+ deleteLockIfUnused(resolved);
97
99
  }
98
100
  }
99
101
  }
@@ -1,19 +1,31 @@
1
- import { getConfig } from '#internals.js';
2
-
3
- const { timeZone } = await getConfig();
4
-
5
- export const getTimeString = (timestamp, TIME_ZONE = timeZone) => {
6
- const date = new Date(timestamp * 1000);
7
- const options = {
8
- year: 'numeric',
9
- month: 'long',
10
- day: 'numeric',
11
- hour: '2-digit',
12
- minute: '2-digit',
13
- second: '2-digit',
14
- hour12: false,
15
- TIME_ZONE
16
- };
17
- const result = date.toLocaleDateString('en-US', options)
18
- return result.split(' at ')
19
- }
1
+ import { getConfig } from '#internals.js';
2
+
3
+ const { timeZone: defaultTimeZone } = await getConfig();
4
+
5
+ const formatters = new Map();
6
+
7
+ function getFormatter(tz) {
8
+ let formatter = formatters.get(tz);
9
+ if (!formatter) {
10
+ formatter = new Intl.DateTimeFormat('en-US', {
11
+ year: 'numeric',
12
+ month: 'long',
13
+ day: 'numeric',
14
+ hour: '2-digit',
15
+ minute: '2-digit',
16
+ second: '2-digit',
17
+ hour12: false,
18
+ timeZone: tz,
19
+ });
20
+ formatters.set(tz, formatter);
21
+ }
22
+ return formatter;
23
+ }
24
+
25
+ export const getTimeString = (timestamp, timeZone = defaultTimeZone) => {
26
+ if (!timestamp) return ['', ''];
27
+ const date = new Date(timestamp * 1000);
28
+ const formatter = getFormatter(timeZone);
29
+ const result = formatter.format(date);
30
+ return result.split(' at ');
31
+ };
@@ -1,5 +1,11 @@
1
- export const isString = x => typeof x === "string";
2
-
3
- export const toNumber = x => (x && typeof x.toNumber === "function") ? x.toNumber() : x;
4
-
5
- export const toBase64 = x => x ? Buffer.from(x).toString("base64") : x;
1
+ export const isString = (x) => typeof x === 'string';
2
+
3
+ export const toNumber = (x) => (x && typeof x.toNumber === 'function' ? x.toNumber() : x);
4
+
5
+ export const toBase64 = (x) => {
6
+ if (!x) return x;
7
+ if (typeof x === 'string') return x;
8
+ if (Buffer.isBuffer(x)) return x.toString('base64');
9
+ if (x instanceof Uint8Array) return Buffer.from(x.buffer, x.byteOffset, x.byteLength).toString('base64');
10
+ return Buffer.from(x).toString('base64');
11
+ };