|
| 1 | +# Protoblast Development Guide |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Protoblast is a native object expansion library that adds utility methods to JavaScript built-in types. It provides the foundation for the entire Alchemy ecosystem, including the class inheritance system used by AlchemyMVC, Hawkejs, and other projects. |
| 6 | + |
| 7 | +## Commands |
| 8 | +- Run tests: `npm test` |
| 9 | +- Run with coverage: `npm run coverage` |
| 10 | + |
| 11 | +## Usage Modes |
| 12 | + |
| 13 | +### Modify Native Prototypes (Default) |
| 14 | +```javascript |
| 15 | +// Adds methods directly to native objects |
| 16 | +require('protoblast')(); |
| 17 | + |
| 18 | +'hello world'.after('hello'); // ' world' |
| 19 | +``` |
| 20 | + |
| 21 | +### Bound Functions Mode |
| 22 | +```javascript |
| 23 | +// Does not modify native objects - safer for libraries |
| 24 | +const Blast = require('protoblast')(false); |
| 25 | + |
| 26 | +Blast.Bound.String.after('hello world', 'hello'); // ' world' |
| 27 | +``` |
| 28 | + |
| 29 | +## Core Concepts |
| 30 | + |
| 31 | +### Global Variables (when loaded) |
| 32 | +- `Blast` - The main Protoblast instance |
| 33 | +- `Classes` - All registered classes (e.g., `Classes.Informer`, `Classes.Pledge`) |
| 34 | +- `Fn` - Alias for `Blast.Collection.Function` |
| 35 | +- `Obj` - Alias for `Blast.Bound.Object` |
| 36 | +- `Bound` - Contains bound versions of all methods |
| 37 | + |
| 38 | +These variables are actually NOT globals, they're just made available via the custom module wrapper used by Protoblast. |
| 39 | + |
| 40 | +### Environment Detection |
| 41 | +```javascript |
| 42 | +Blast.isBrowser // Running in browser |
| 43 | +Blast.isNode // Running in Node.js |
| 44 | +Blast.isServer // Node.js or Bun |
| 45 | +Blast.isBun // Running in Bun |
| 46 | +``` |
| 47 | + |
| 48 | +## Adding Methods to Native Types |
| 49 | + |
| 50 | +Each type file (e.g., `lib/string.js`) uses definers created at the top: |
| 51 | + |
| 52 | +```javascript |
| 53 | +const defStat = Blast.createStaticDefiner('String'), // For String.method() |
| 54 | + defProto = Blast.createProtoDefiner('String'); // For 'str'.method() |
| 55 | + |
| 56 | +// Add a static method: String.isLetter(char) |
| 57 | +defStat(function isLetter(character) { |
| 58 | + return character.toUpperCase() !== character.toLowerCase(); |
| 59 | +}); |
| 60 | + |
| 61 | +// Add a prototype method: 'hello'.after('he') |
| 62 | +defProto(function after(needle) { |
| 63 | + // implementation |
| 64 | +}); |
| 65 | +``` |
| 66 | + |
| 67 | +For shimming existing methods only when they don't exist: |
| 68 | +```javascript |
| 69 | +defProto(function trim() { ... }, true); // true = shim only |
| 70 | +``` |
| 71 | + |
| 72 | +## Browser Builds |
| 73 | + |
| 74 | +Code between `// PROTOBLAST START CUT` and `// PROTOBLAST END CUT` is stripped from browser builds. Use this for Node.js-only code: |
| 75 | + |
| 76 | +```javascript |
| 77 | +// PROTOBLAST START CUT |
| 78 | +const fs = require('fs'); |
| 79 | +// Node-only implementation |
| 80 | +// PROTOBLAST END CUT |
| 81 | +``` |
| 82 | + |
| 83 | +The `//_PROTOBLAST_ENV_//` marker is replaced with environment configuration during builds. |
| 84 | + |
| 85 | +## Class Inheritance System |
| 86 | + |
| 87 | +Protoblast provides `Function.inherits()` which is the foundation for all Alchemy projects: |
| 88 | + |
| 89 | +```javascript |
| 90 | +// Create a new class |
| 91 | +const Animal = Function.inherits(function Animal(name) { |
| 92 | + this.name = name; |
| 93 | +}); |
| 94 | + |
| 95 | +// Add instance method - function name becomes method name |
| 96 | +Animal.setMethod(function speak() { |
| 97 | + return this.name + ' makes a sound'; |
| 98 | +}); |
| 99 | + |
| 100 | +// Add static method |
| 101 | +Animal.setStatic(function create(name) { |
| 102 | + return new this(name); |
| 103 | +}); |
| 104 | + |
| 105 | +// Add getter/setter property |
| 106 | +Animal.setProperty(function upperName() { |
| 107 | + return this.name.toUpperCase(); |
| 108 | +}); |
| 109 | + |
| 110 | +// Lazy-initialized property (computed once on first access) |
| 111 | +Animal.prepareProperty('cache', function() { |
| 112 | + return new Map(); |
| 113 | +}); |
| 114 | + |
| 115 | +// Inherit from Animal with namespace |
| 116 | +const Dog = Function.inherits('Animal', 'MyApp', function Dog(name) { |
| 117 | + Dog.super.call(this, name); |
| 118 | +}); |
| 119 | + |
| 120 | +// Deferred setup (runs after class hierarchy is ready) |
| 121 | +Dog.constitute(function setup() { |
| 122 | + // Configure class after all parents are ready |
| 123 | +}); |
| 124 | + |
| 125 | +// Mark class as abstract |
| 126 | +Animal.makeAbstractClass(); |
| 127 | + |
| 128 | +// Get all child classes |
| 129 | +Animal.getDescendants(); // Returns array of child classes |
| 130 | +Animal.getDescendantsDict(); // Returns {type_name: ChildClass} map |
| 131 | +Animal.getDescendant('dog'); // Get specific child by type_name |
| 132 | +``` |
| 133 | + |
| 134 | +### Class Path Format |
| 135 | + |
| 136 | +```javascript |
| 137 | +// Function.inherits(parent, namespace, constructor) |
| 138 | +Function.inherits('Alchemy.Base', 'MyApp', function MyClass() {}); |
| 139 | +// -> Classes.MyApp.MyClass |
| 140 | + |
| 141 | +// Without namespace: class goes in parent's namespace |
| 142 | +Function.inherits('Alchemy.Base', function MyClass() {}); |
| 143 | +// -> Classes.Alchemy.MyClass |
| 144 | +``` |
| 145 | + |
| 146 | +**Namespace vs class as parent:** |
| 147 | + |
| 148 | +If the parent path points to a namespace, it inherits from `Namespace.Namespace`: |
| 149 | +```javascript |
| 150 | +// Alchemy.Widget is a namespace containing Alchemy.Widget.Widget |
| 151 | +Function.inherits('Alchemy.Widget', function MyWidget() {}); |
| 152 | +// Inherits from: Alchemy.Widget.Widget |
| 153 | +// Result: Classes.Alchemy.Widget.MyWidget |
| 154 | +``` |
| 155 | + |
| 156 | +If the parent path points to a class directly, child goes in parent's namespace: |
| 157 | +```javascript |
| 158 | +// Alchemy.Base is a class at Classes.Alchemy.Base (not a namespace) |
| 159 | +Function.inherits('Alchemy.Base', function MyClass() {}); |
| 160 | +// Result: Classes.Alchemy.MyClass (NOT Classes.Alchemy.Base.MyClass) |
| 161 | +``` |
| 162 | + |
| 163 | +## Key Classes |
| 164 | + |
| 165 | +### Informer |
| 166 | +Event emitter with queryable filters: |
| 167 | +```javascript |
| 168 | +const emitter = new Blast.Classes.Informer(); |
| 169 | + |
| 170 | +emitter.on('event', callback); // Simple event |
| 171 | +emitter.on({type: 'user', action: 'login'}, callback); // Filter-based |
| 172 | +emitter.once('ready', callback); // Fire once |
| 173 | +emitter.after('ready', callback); // Fire after event (or immediately if seen) |
| 174 | +emitter.emit('event', data); |
| 175 | +emitter.hasBeenSeen('ready'); |
| 176 | +emitter.unsee('ready'); |
| 177 | +``` |
| 178 | + |
| 179 | +### Pledge |
| 180 | +Promise implementation with extra features: |
| 181 | +```javascript |
| 182 | +const Pledge = Blast.Classes.Pledge; |
| 183 | + |
| 184 | +const pledge = new Pledge((resolve, reject) => resolve('value')); |
| 185 | + |
| 186 | +pledge.state; // 0=pending, 1=resolved, 2=rejected |
| 187 | +pledge.isPending(); |
| 188 | +pledge.done((err, result) => {}); // Node-style callback |
| 189 | + |
| 190 | +// Progress tracking |
| 191 | +pledge.addProgressPart(10); |
| 192 | +pledge.reportProgressPart(1); |
| 193 | + |
| 194 | +// Variants |
| 195 | +new Pledge.Lazy(executor); // Starts on first .then() |
| 196 | +new Pledge.Timeout(executor, 5000); // Auto-rejects after timeout |
| 197 | +new Pledge.Swift(executor); // Synchronous when possible |
| 198 | +``` |
| 199 | + |
| 200 | +### Swift (High-Performance Pledge) |
| 201 | +For performance-critical code where synchronous execution is preferred: |
| 202 | +```javascript |
| 203 | +const Swift = Blast.Classes.Pledge.Swift; |
| 204 | + |
| 205 | +Swift.execute(valueOrPromise); // Returns value directly if resolved |
| 206 | +Swift.waterfall(task1, task2, task3); |
| 207 | +Swift.parallel([task1, task2]); |
| 208 | +Swift.done(value, (err, result) => {}); |
| 209 | +``` |
| 210 | + |
| 211 | +## Async Flow Control |
| 212 | + |
| 213 | +```javascript |
| 214 | +// Sequential execution |
| 215 | +Function.series([task1, task2, task3], callback); |
| 216 | +await Function.series([task1, task2]); |
| 217 | + |
| 218 | +// Parallel execution (optionally limited) |
| 219 | +Function.parallel([task1, task2, task3], callback); |
| 220 | +Function.parallel(2, [task1, task2, task3], callback); // Max 2 concurrent |
| 221 | + |
| 222 | +// Pass results through chain |
| 223 | +Function.waterfall( |
| 224 | + (next) => next(null, 'value1'), |
| 225 | + (prev, next) => next(null, prev + 'value2'), |
| 226 | + callback |
| 227 | +); |
| 228 | + |
| 229 | +// Loop constructs |
| 230 | +Function.while(testFn, taskFn, callback); |
| 231 | +Function.forEach(data, (value, key, next) => {}, callback); |
| 232 | +``` |
| 233 | + |
| 234 | +## Lifecycle Hooks |
| 235 | + |
| 236 | +```javascript |
| 237 | +Blast.ready(() => { /* All Blast classes available */ }); |
| 238 | +Blast.loaded(() => { /* All scripts loaded */ }); |
| 239 | +Blast.queueTick(fn); |
| 240 | +Blast.queueImmediate(fn); |
| 241 | +``` |
| 242 | + |
| 243 | +## Directory Structure |
| 244 | + |
| 245 | +``` |
| 246 | +lib/ |
| 247 | +├── init.js # Entry point, core initialization |
| 248 | +├── blast.js # Blast instance methods |
| 249 | +├── function_inheritance.js # Function.inherits() and class system |
| 250 | +├── function_flow.js # series, parallel, waterfall, etc. |
| 251 | +├── informer.js # Event emitter class |
| 252 | +├── pledge.js # Promise implementation |
| 253 | +├── string.js # String prototype extensions |
| 254 | +├── array.js # Array prototype extensions |
| 255 | +├── object.js # Object utilities |
| 256 | +├── date.js # Date extensions |
| 257 | +├── json.js # JSON-Dry integration |
| 258 | +└── ... # See lib/ for full list |
| 259 | +``` |
| 260 | + |
| 261 | +## JSON-Dry Integration |
| 262 | + |
| 263 | +Protoblast integrates [json-dry](https://github.com/11ways/json-dry) for serialization and cloning. |
| 264 | + |
| 265 | +```javascript |
| 266 | +// Serialize to JSON string (preserves references, class instances) |
| 267 | +let dried = JSON.dry(obj); |
| 268 | +let undried = JSON.undry(dried); |
| 269 | + |
| 270 | +// Clone (returns live object, not string) |
| 271 | +let cloned = JSON.clone(obj); |
| 272 | +let prepared = JSON.clone(obj, 'toHawkejs'); // With custom method |
| 273 | +``` |
| 274 | + |
| 275 | +**Custom method cloning** (`JSON.clone(obj, 'methodName')`): |
| 276 | +- For each object, if `obj.methodName` exists, it's called with `(weakmap, ...extra_args)` |
| 277 | +- Return value replaces the original in the clone |
| 278 | +- This is how Hawkejs transforms server objects to client-safe versions |
| 279 | + |
| 280 | +### Implementing Serialization |
| 281 | + |
| 282 | +```javascript |
| 283 | +// Instance method - what data to serialize |
| 284 | +MyClass.prototype.toDry = function() { |
| 285 | + return { value: { name: this.name } }; |
| 286 | +}; |
| 287 | + |
| 288 | +// Static method - reconstruct from serialized data |
| 289 | +MyClass.unDry = function(value) { |
| 290 | + return new MyClass(value); |
| 291 | +}; |
| 292 | + |
| 293 | +JSON.registerClass(MyClass); // Required for unDry |
| 294 | +``` |
| 295 | + |
| 296 | +## Gotchas |
| 297 | + |
| 298 | +1. **`Function.inherits()` signature:** First arg can be parent class name (string), parent class (function), or array (multiple inheritance) |
| 299 | + |
| 300 | +2. **`constitute()` is queued, not overridden:** Multiple `constitute()` calls are all queued and run in order. No `super` call needed - parent constitutes run automatically before child constitutes. |
| 301 | + |
| 302 | +3. **`postInherit()` vs `constitute()`:** `postInherit()` runs immediately after inheritance, `constitute()` runs after Blast.loaded() |
| 303 | + |
| 304 | +4. **Pledge vs Promise:** Pledges have extra methods like `.done()`, progress tracking, and can be synchronous with Swift |
| 305 | + |
| 306 | +5. **Bound vs Prototype:** When not modifying prototypes, use `Blast.Bound.String.method(str, args)` instead of `str.method(args)` |
| 307 | + |
| 308 | +6. **Series/Parallel tasks:** Tasks receive a `next` callback - call it with `(err, result)` pattern |
| 309 | + |
| 310 | +7. **Class type_name:** Automatically derived from class name (e.g., `MyClass` becomes `my_class`) - used in `getDescendantsDict()` |
| 311 | + |
| 312 | +8. **Namespace functions:** A namespace like `Classes.MyApp` can be called as a function - it instantiates `Classes.MyApp.MyApp` |
| 313 | + |
| 314 | +9. **Namespace as parent:** If parent path is a namespace (not a class), it inherits from `Namespace.Namespace` (e.g., `'Alchemy.Widget'` -> `Alchemy.Widget.Widget`) |
| 315 | + |
| 316 | +10. **Namespace argument creates path:** `Function.inherits('Parent', 'A.B.C', fn)` registers class at `Classes.A.B.C.ClassName` |
| 317 | + |
| 318 | +11. **Class groups are inherited:** `startNewGroup()` creates a group attached to that class. Child classes inherit group membership unless they call `startNewGroup()` again |
| 319 | + |
| 320 | +12. **`JSON.clone` vs `JSON.dry`:** `clone()` returns a live object; `dry()` returns a JSON string. Clone with custom method (`clone(obj, 'toHawkejs')`) is used for object transformation, not serialization |
| 321 | + |
| 322 | +13. **Custom clone method signature:** Methods like `toHawkejs` receive `(wm, ...extra_args)` where `wm` is a WeakMap - pass it when cloning nested objects to preserve reference identity |
| 323 | + |
| 324 | +14. **`FixedDecimal.ensure()` uses the instance's scale:** |
| 325 | + ```javascript |
| 326 | + let rate = new FixedDecimal('0.05', 2); // scale=2 |
| 327 | + let divisor = rate.ensure(0.001); // Returns 0.00 (rounded to scale 2!) |
| 328 | + // Use Decimal.ensure() to preserve precision of small values |
| 329 | + let divisor = Decimal.ensure(0.001); // Keeps full precision |
| 330 | + ``` |
0 commit comments